From 751b2056181b8f7df8d492803b62210131f35bb5 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 5 Mar 2023 16:59:24 +0200 Subject: [PATCH 001/208] initial commit --- .../freqai/base_models/BasePytorchModel.py | 69 +++++++++++++ .../freqai/base_models/PytorchModelTrainer.py | 51 ++++++++++ freqtrade/freqai/data_drawer.py | 7 +- .../PytorchClassifierMultiTarget.py | 97 +++++++++++++++++++ .../prediction_models/PytorchMLPModel.py | 31 ++++++ 5 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 freqtrade/freqai/base_models/BasePytorchModel.py create mode 100644 freqtrade/freqai/base_models/PytorchModelTrainer.py create mode 100644 freqtrade/freqai/prediction_models/PytorchClassifierMultiTarget.py create mode 100644 freqtrade/freqai/prediction_models/PytorchMLPModel.py diff --git a/freqtrade/freqai/base_models/BasePytorchModel.py b/freqtrade/freqai/base_models/BasePytorchModel.py new file mode 100644 index 000000000..da0590a36 --- /dev/null +++ b/freqtrade/freqai/base_models/BasePytorchModel.py @@ -0,0 +1,69 @@ +import logging +from time import time +from typing import Any, Dict + +import torch +from pandas import DataFrame + +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.freqai_interface import IFreqaiModel + +logger = logging.getLogger(__name__) + + +class BasePytorchModel(IFreqaiModel): + """ + Base class for TensorFlow type models. + User *must* inherit from this class and set fit() and predict(). + """ + + def __init__(self, **kwargs): + super().__init__(config=kwargs['config']) + self.dd.model_type = 'pytorch' + self.device = 'cuda' if torch.cuda.is_available() else 'cpu' + + def train( + self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs + ) -> Any: + """ + Filter the training data and train a model to it. Train makes heavy use of the datakitchen + for storing, saving, loading, and analyzing the data. + :param unfiltered_df: Full dataframe for the current training period + :param metadata: pair metadata from strategy. + :return: + :model: Trained model which can be used to inference (self.predict) + """ + + logger.info(f"-------------------- Starting training {pair} --------------------") + + start_time = time() + + features_filtered, labels_filtered = dk.filter_features( + unfiltered_df, + dk.training_features_list, + dk.label_list, + training_filter=True, + ) + + # split data into train/test data. + data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered) + if not self.freqai_info.get("fit_live_predictions", 0) or not self.live: + dk.fit_labels() + # normalize all data based on train_dataset only + data_dictionary = dk.normalize_data(data_dictionary) + + # optional additional data cleaning/analysis + self.data_cleaning_train(dk) + + logger.info( + f"Training model on {len(dk.data_dictionary['train_features'].columns)} features" + ) + logger.info(f"Training model on {len(data_dictionary['train_features'])} data points") + + model = self.fit(data_dictionary, dk) + end_time = time() + + logger.info(f"-------------------- Done training {pair} " + f"({end_time - start_time:.2f} secs) --------------------") + + return model diff --git a/freqtrade/freqai/base_models/PytorchModelTrainer.py b/freqtrade/freqai/base_models/PytorchModelTrainer.py new file mode 100644 index 000000000..43a37baf2 --- /dev/null +++ b/freqtrade/freqai/base_models/PytorchModelTrainer.py @@ -0,0 +1,51 @@ +import logging +from pathlib import Path +from typing import Dict + +import torch +import torch.nn as nn + +logger = logging.getLogger(__name__) + + +class PytorchModelTrainer: + def __init__(self, model: nn.Module, optimizer, init_model: Dict): + self.model = model + self.optimizer = optimizer + if init_model: + self.load_from_checkpoint(init_model) + + def fit(self, tensor_dictionary, max_iters, batch_size): + for iter in range(max_iters): + + # todo add validation evaluation here + + xb, yb = self.get_batch(tensor_dictionary, 'train', batch_size) + logits, loss = self.model(xb, yb) + + self.optimizer.zero_grad(set_to_none=True) + loss.backward() + self.optimizer.step() + + def save(self, path): + torch.save({ + 'model_state_dict': self.model.state_dict(), + 'optimizer_state_dict': self.optimizer.state_dict(), + }, path) + + def load_from_file(self, path: Path): + checkpoint = torch.load(path) + return self.load_from_checkpoint(checkpoint) + + def load_from_checkpoint(self, checkpoint: Dict): + self.model.load_state_dict(checkpoint['model_state_dict']) + self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) + return self + + @staticmethod + def get_batch(tensor_dictionary: Dict, split: str, batch_size: int): + ix = torch.randint(len(tensor_dictionary[f'{split}_labels']), (batch_size,)) + x = tensor_dictionary[f'{split}_features'][ix] + y = tensor_dictionary[f'{split}_labels'][ix] + return x, y + diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 14986d854..d167a39eb 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -446,7 +446,9 @@ class FreqaiDataDrawer: dump(model, save_path / f"{dk.model_filename}_model.joblib") elif self.model_type == 'keras': model.save(save_path / f"{dk.model_filename}_model.h5") - elif 'stable_baselines' in self.model_type or 'sb3_contrib' == self.model_type: + elif 'stable_baselines' in self.model_type or\ + 'sb3_contrib' == self.model_type or\ + 'pytorch' == self.model_type: model.save(save_path / f"{dk.model_filename}_model.zip") if dk.svm_model is not None: @@ -537,6 +539,9 @@ class FreqaiDataDrawer: self.model_type, self.freqai_info['rl_config']['model_type']) MODELCLASS = getattr(mod, self.freqai_info['rl_config']['model_type']) model = MODELCLASS.load(dk.data_path / f"{dk.model_filename}_model") + elif self.model_type == 'pytorch': + import torch + model = torch.load(dk.data_path / f"{dk.model_filename}_model.zip") if Path(dk.data_path / f"{dk.model_filename}_svm_model.joblib").is_file(): dk.svm_model = load(dk.data_path / f"{dk.model_filename}_svm_model.joblib") diff --git a/freqtrade/freqai/prediction_models/PytorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PytorchClassifierMultiTarget.py new file mode 100644 index 000000000..e4a090bb4 --- /dev/null +++ b/freqtrade/freqai/prediction_models/PytorchClassifierMultiTarget.py @@ -0,0 +1,97 @@ +import logging + +from typing import Dict +from typing import Any, Dict, Tuple +import numpy.typing as npt + +import numpy as np +import pandas as pd +import torch +from pandas import DataFrame + +from torch.nn import functional as F + +from freqtrade.freqai.base_models.BasePytorchModel import BasePytorchModel +from freqtrade.freqai.base_models.PytorchModelTrainer import PytorchModelTrainer +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.PytorchMLPModel import MLP + +logger = logging.getLogger(__name__) + + +class PytorchClassifierMultiTarget(BasePytorchModel): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + # todo move to config + self.n_hidden = 1024 + self.labels = ['0.0', '1.0', '2.0'] + self.max_iters = 100 + self.batch_size = 64 + self.learning_rate = 3e-4 + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param tensor_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + """ + n_features = data_dictionary['train_features'].shape[-1] + tensor_dictionary = self.convert_data_to_tensors(data_dictionary) + model = MLP( + input_dim=n_features, + hidden_dim=self.n_hidden, + output_dim=len(self.labels) + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + init_model = self.get_init_model(dk.pair) + trainer = PytorchModelTrainer(model, optimizer, init_model=init_model) + trainer.fit(tensor_dictionary, self.max_iters, self.batch_size) + return trainer + + def predict( + self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs + ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: + """ + Filter the prediction features data and predict with it. + :param unfiltered_df: Full dataframe for the current backtest period. + :return: + :pred_df: dataframe containing the predictions + :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove + data (NaNs) or felt uncertain about data (PCA and DI index) + """ + + dk.find_features(unfiltered_df) + filtered_df, _ = dk.filter_features( + unfiltered_df, dk.training_features_list, training_filter=False + ) + filtered_df = dk.normalize_data_from_metadata(filtered_df) + dk.data_dictionary["prediction_features"] = filtered_df + + self.data_cleaning_predict(dk) + dk.data_dictionary["prediction_features"] = torch.tensor( + dk.data_dictionary["prediction_features"].values + ).to(self.device) + + logits, _ = self.model.model(dk.data_dictionary["prediction_features"]) + probs = F.softmax(logits, dim=-1) + label_ints = torch.argmax(probs, dim=-1) + + pred_df_prob = DataFrame(probs.detach().numpy(), columns=self.labels) + pred_df = DataFrame(label_ints, columns=dk.label_list).astype(float).astype(str) + pred_df = pd.concat([pred_df, pred_df_prob], axis=1) + return (pred_df, dk.do_predict) + + def convert_data_to_tensors(self, data_dictionary: Dict) -> Dict: + tensor_dictionary = {} + for split in ['train', 'test']: + tensor_dictionary[f'{split}_features'] = torch.tensor( + data_dictionary[f'{split}_features'].values + ).to(self.device) + tensor_dictionary[f'{split}_labels'] = torch.tensor( + data_dictionary[f'{split}_labels'].astype(float).values + ).long().to(self.device) + + return tensor_dictionary diff --git a/freqtrade/freqai/prediction_models/PytorchMLPModel.py b/freqtrade/freqai/prediction_models/PytorchMLPModel.py new file mode 100644 index 000000000..c70a21395 --- /dev/null +++ b/freqtrade/freqai/prediction_models/PytorchMLPModel.py @@ -0,0 +1,31 @@ +import logging + + +import torch +import torch.nn as nn +from torch.nn import functional as F + +logger = logging.getLogger(__name__) + + +class MLP(nn.Module): + def __init__(self, input_dim, hidden_dim, output_dim): + super(MLP, self).__init__() + self.input_layer = nn.Linear(input_dim, hidden_dim) + self.hidden_layer = nn.Linear(hidden_dim, hidden_dim) + self.output_layer = nn.Linear(hidden_dim, output_dim) + self.relu = nn.ReLU() + self.dropout = nn.Dropout(p=0.2) + + def forward(self, x, targets=None): + x = self.relu(self.input_layer(x)) + x = self.dropout(x) + x = self.relu(self.hidden_layer(x)) + x = self.dropout(x) + logits = self.output_layer(x) + + if targets is None: + return logits, None + + loss = F.cross_entropy(logits, targets.squeeze()) + return logits, loss From b1ac2bf515637e565c038c910c82657ed069482c Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 6 Mar 2023 16:16:45 +0200 Subject: [PATCH 002/208] use data loader, add evaluation on epoch --- ...asePytorchModel.py => BasePyTorchModel.py} | 5 +- .../freqai/base_models/PyTorchModelTrainer.py | 136 ++++++++++++++++++ .../freqai/base_models/PytorchModelTrainer.py | 51 ------- ...get.py => PyTorchClassifierMultiTarget.py} | 50 ++++--- ...{PytorchMLPModel.py => PyTorchMLPModel.py} | 16 +-- 5 files changed, 167 insertions(+), 91 deletions(-) rename freqtrade/freqai/base_models/{BasePytorchModel.py => BasePyTorchModel.py} (94%) create mode 100644 freqtrade/freqai/base_models/PyTorchModelTrainer.py delete mode 100644 freqtrade/freqai/base_models/PytorchModelTrainer.py rename freqtrade/freqai/prediction_models/{PytorchClassifierMultiTarget.py => PyTorchClassifierMultiTarget.py} (70%) rename freqtrade/freqai/prediction_models/{PytorchMLPModel.py => PyTorchMLPModel.py} (60%) diff --git a/freqtrade/freqai/base_models/BasePytorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py similarity index 94% rename from freqtrade/freqai/base_models/BasePytorchModel.py rename to freqtrade/freqai/base_models/BasePyTorchModel.py index da0590a36..1074ddeea 100644 --- a/freqtrade/freqai/base_models/BasePytorchModel.py +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -1,6 +1,6 @@ import logging from time import time -from typing import Any, Dict +from typing import Any import torch from pandas import DataFrame @@ -11,7 +11,7 @@ from freqtrade.freqai.freqai_interface import IFreqaiModel logger = logging.getLogger(__name__) -class BasePytorchModel(IFreqaiModel): +class BasePyTorchModel(IFreqaiModel): """ Base class for TensorFlow type models. User *must* inherit from this class and set fit() and predict(). @@ -29,7 +29,6 @@ class BasePytorchModel(IFreqaiModel): Filter the training data and train a model to it. Train makes heavy use of the datakitchen for storing, saving, loading, and analyzing the data. :param unfiltered_df: Full dataframe for the current training period - :param metadata: pair metadata from strategy. :return: :model: Trained model which can be used to inference (self.predict) """ diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py new file mode 100644 index 000000000..13c5ffe74 --- /dev/null +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -0,0 +1,136 @@ +import logging +from pathlib import Path +from typing import Dict + +import torch +import torch.nn as nn +from torch.utils.data import DataLoader +from torch.utils.data import TensorDataset +import pandas as pd + +logger = logging.getLogger(__name__) + + +class PyTorchModelTrainer: + def __init__( + self, + model: nn.Module, + optimizer: nn.Module, + criterion: nn.Module, + device: str, + batch_size: int, + max_iters: int, + eval_iters: int, + init_model: Dict + ): + self.model = model + self.optimizer = optimizer + self.criterion = criterion + self.device = device + self.max_iters = max_iters + self.batch_size = batch_size + self.eval_iters = eval_iters + + if init_model: + self.load_from_checkpoint(init_model) + + def fit(self, data_dictionary: Dict[str, pd.DataFrame]): + data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary) + epochs = self.calc_n_epochs( + n_obs=len(data_dictionary['train_features']), + batch_size=self.batch_size, + n_iters=self.max_iters + ) + for epoch in range(epochs): + # evaluation + losses = self.estimate_loss(data_loaders_dictionary, data_dictionary) + logger.info( + f"epoch ({epoch}/{epochs}):" + f" train loss {losses['train']:.4f} ; test loss {losses['test']:.4f}" + ) + # training + for batch_data in data_loaders_dictionary['train']: + xb, yb = batch_data + xb = xb.to(self.device) # type: ignore + yb = yb.to(self.device) + yb_pred = self.model(xb) + loss = self.criterion(yb_pred, yb) + + self.optimizer.zero_grad(set_to_none=True) + loss.backward() + self.optimizer.step() + + @torch.no_grad() + def estimate_loss( + self, + data_loader_dictionary: Dict[str, DataLoader], + data_dictionary: Dict[str, pd.DataFrame] + ) -> Dict[str, float]: + + self.model.eval() + epochs = self.calc_n_epochs( + n_obs=len(data_dictionary[f'test_features']), + batch_size=self.batch_size, + n_iters=self.eval_iters + ) + loss_dictionary = {} + for split in ['train', 'test']: + losses = torch.zeros(epochs) + for i, batch in enumerate(data_loader_dictionary[split]): + xb, yb = batch + xb = xb.to(self.device) + yb = yb.to(self.device) + yb_pred = self.model(xb) + loss = self.criterion(yb_pred, yb) + losses[i] = loss.item() + + loss_dictionary[split] = losses.mean() + + self.model.train() + return loss_dictionary + + def create_data_loaders_dictionary( + self, + data_dictionary: Dict[str, pd.DataFrame] + ) -> Dict[str, DataLoader]: + data_loader_dictionary = {} + for split in ['train', 'test']: + labels_shape = data_dictionary[f'{split}_labels'].shape + labels_view = labels_shape[0] if labels_shape[1] == 1 else labels_shape + dataset = TensorDataset( + torch.from_numpy(data_dictionary[f'{split}_features'].values).float(), + torch.from_numpy(data_dictionary[f'{split}_labels'].astype(float).values) + .long() + .view(labels_view) + ) + data_loader = DataLoader( + dataset, + batch_size=self.batch_size, + shuffle=True, + drop_last=True, + num_workers=0, + ) + data_loader_dictionary[split] = data_loader + + return data_loader_dictionary + + @staticmethod + def calc_n_epochs(n_obs: int, batch_size: int, n_iters: int) -> int: + n_batches = n_obs // batch_size + epochs = n_iters // n_batches + return epochs + + def save(self, path: Path): + torch.save({ + 'model_state_dict': self.model.state_dict(), + 'optimizer_state_dict': self.optimizer.state_dict(), + }, path) + + def load_from_file(self, path: Path): + checkpoint = torch.load(path) + return self.load_from_checkpoint(checkpoint) + + def load_from_checkpoint(self, checkpoint: Dict): + self.model.load_state_dict(checkpoint['model_state_dict']) + self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) + return self diff --git a/freqtrade/freqai/base_models/PytorchModelTrainer.py b/freqtrade/freqai/base_models/PytorchModelTrainer.py deleted file mode 100644 index 43a37baf2..000000000 --- a/freqtrade/freqai/base_models/PytorchModelTrainer.py +++ /dev/null @@ -1,51 +0,0 @@ -import logging -from pathlib import Path -from typing import Dict - -import torch -import torch.nn as nn - -logger = logging.getLogger(__name__) - - -class PytorchModelTrainer: - def __init__(self, model: nn.Module, optimizer, init_model: Dict): - self.model = model - self.optimizer = optimizer - if init_model: - self.load_from_checkpoint(init_model) - - def fit(self, tensor_dictionary, max_iters, batch_size): - for iter in range(max_iters): - - # todo add validation evaluation here - - xb, yb = self.get_batch(tensor_dictionary, 'train', batch_size) - logits, loss = self.model(xb, yb) - - self.optimizer.zero_grad(set_to_none=True) - loss.backward() - self.optimizer.step() - - def save(self, path): - torch.save({ - 'model_state_dict': self.model.state_dict(), - 'optimizer_state_dict': self.optimizer.state_dict(), - }, path) - - def load_from_file(self, path: Path): - checkpoint = torch.load(path) - return self.load_from_checkpoint(checkpoint) - - def load_from_checkpoint(self, checkpoint: Dict): - self.model.load_state_dict(checkpoint['model_state_dict']) - self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) - return self - - @staticmethod - def get_batch(tensor_dictionary: Dict, split: str, batch_size: int): - ix = torch.randint(len(tensor_dictionary[f'{split}_labels']), (batch_size,)) - x = tensor_dictionary[f'{split}_features'][ix] - y = tensor_dictionary[f'{split}_labels'][ix] - return x, y - diff --git a/freqtrade/freqai/prediction_models/PytorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py similarity index 70% rename from freqtrade/freqai/prediction_models/PytorchClassifierMultiTarget.py rename to freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index e4a090bb4..9504fffb8 100644 --- a/freqtrade/freqai/prediction_models/PytorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -1,6 +1,5 @@ import logging -from typing import Dict from typing import Any, Dict, Tuple import numpy.typing as npt @@ -8,28 +7,29 @@ import numpy as np import pandas as pd import torch from pandas import DataFrame - from torch.nn import functional as F -from freqtrade.freqai.base_models.BasePytorchModel import BasePytorchModel -from freqtrade.freqai.base_models.PytorchModelTrainer import PytorchModelTrainer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.PytorchMLPModel import MLP + +from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel +from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer +from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel + logger = logging.getLogger(__name__) -class PytorchClassifierMultiTarget(BasePytorchModel): +class PyTorchClassifierMultiTarget(BasePyTorchModel): def __init__(self, **kwargs): super().__init__(**kwargs) - # todo move to config - self.n_hidden = 1024 self.labels = ['0.0', '1.0', '2.0'] + self.n_hidden = 1024 self.max_iters = 100 self.batch_size = 64 self.learning_rate = 3e-4 + self.eval_iters = 10 def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ @@ -38,17 +38,27 @@ class PytorchClassifierMultiTarget(BasePytorchModel): all the training and test data/labels. """ n_features = data_dictionary['train_features'].shape[-1] - tensor_dictionary = self.convert_data_to_tensors(data_dictionary) - model = MLP( + + model = PyTorchMLPModel( input_dim=n_features, hidden_dim=self.n_hidden, output_dim=len(self.labels) ) model.to(self.device) optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.CrossEntropyLoss() init_model = self.get_init_model(dk.pair) - trainer = PytorchModelTrainer(model, optimizer, init_model=init_model) - trainer.fit(tensor_dictionary, self.max_iters, self.batch_size) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + device=self.device, + batch_size=self.batch_size, + max_iters=self.max_iters, + eval_iters=self.eval_iters, + init_model=init_model + ) + trainer.fit(data_dictionary) return trainer def predict( @@ -73,9 +83,9 @@ class PytorchClassifierMultiTarget(BasePytorchModel): self.data_cleaning_predict(dk) dk.data_dictionary["prediction_features"] = torch.tensor( dk.data_dictionary["prediction_features"].values - ).to(self.device) + ).float().to(self.device) - logits, _ = self.model.model(dk.data_dictionary["prediction_features"]) + logits = self.model.model(dk.data_dictionary["prediction_features"]) probs = F.softmax(logits, dim=-1) label_ints = torch.argmax(probs, dim=-1) @@ -83,15 +93,3 @@ class PytorchClassifierMultiTarget(BasePytorchModel): pred_df = DataFrame(label_ints, columns=dk.label_list).astype(float).astype(str) pred_df = pd.concat([pred_df, pred_df_prob], axis=1) return (pred_df, dk.do_predict) - - def convert_data_to_tensors(self, data_dictionary: Dict) -> Dict: - tensor_dictionary = {} - for split in ['train', 'test']: - tensor_dictionary[f'{split}_features'] = torch.tensor( - data_dictionary[f'{split}_features'].values - ).to(self.device) - tensor_dictionary[f'{split}_labels'] = torch.tensor( - data_dictionary[f'{split}_labels'].astype(float).values - ).long().to(self.device) - - return tensor_dictionary diff --git a/freqtrade/freqai/prediction_models/PytorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py similarity index 60% rename from freqtrade/freqai/prediction_models/PytorchMLPModel.py rename to freqtrade/freqai/prediction_models/PyTorchMLPModel.py index c70a21395..4e1cc32ba 100644 --- a/freqtrade/freqai/prediction_models/PytorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -3,29 +3,23 @@ import logging import torch import torch.nn as nn -from torch.nn import functional as F logger = logging.getLogger(__name__) -class MLP(nn.Module): - def __init__(self, input_dim, hidden_dim, output_dim): - super(MLP, self).__init__() +class PyTorchMLPModel(nn.Module): + def __init__(self, input_dim: int, hidden_dim: int, output_dim: int): + super(PyTorchMLPModel, self).__init__() self.input_layer = nn.Linear(input_dim, hidden_dim) self.hidden_layer = nn.Linear(hidden_dim, hidden_dim) self.output_layer = nn.Linear(hidden_dim, output_dim) self.relu = nn.ReLU() self.dropout = nn.Dropout(p=0.2) - def forward(self, x, targets=None): + def forward(self, x: torch.tensor) -> torch.tensor: x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.relu(self.hidden_layer(x)) x = self.dropout(x) logits = self.output_layer(x) - - if targets is None: - return logits, None - - loss = F.cross_entropy(logits, targets.squeeze()) - return logits, loss + return logits From 348a08f1c41b47601bb5592280a567e7c0225b8b Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 6 Mar 2023 16:41:47 +0200 Subject: [PATCH 003/208] add todo - currently assuming class labels are strings ['0.0', '1.0' .. n_classes]. need to resolve it per ClassifierModel --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 13c5ffe74..03d264371 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -101,7 +101,7 @@ class PyTorchModelTrainer: torch.from_numpy(data_dictionary[f'{split}_features'].values).float(), torch.from_numpy(data_dictionary[f'{split}_labels'].astype(float).values) .long() - .view(labels_view) + .view(labels_view) # todo currently assuming class labels are strings ['0.0', '1.0' .. n_classes]. need to resolve it per ClassifierModel ) data_loader = DataLoader( dataset, From e6e747bcd819b28336dbf4232c6d23226102e6bf Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 6 Mar 2023 17:50:02 +0200 Subject: [PATCH 004/208] reformat code --- .../freqai/base_models/PyTorchModelTrainer.py | 7 ++++-- freqtrade/freqai/data_drawer.py | 23 +++++++++---------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 03d264371..992ad37ef 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -69,7 +69,7 @@ class PyTorchModelTrainer: self.model.eval() epochs = self.calc_n_epochs( - n_obs=len(data_dictionary[f'test_features']), + n_obs=len(data_dictionary['test_features']), batch_size=self.batch_size, n_iters=self.eval_iters ) @@ -101,8 +101,11 @@ class PyTorchModelTrainer: torch.from_numpy(data_dictionary[f'{split}_features'].values).float(), torch.from_numpy(data_dictionary[f'{split}_labels'].astype(float).values) .long() - .view(labels_view) # todo currently assuming class labels are strings ['0.0', '1.0' .. n_classes]. need to resolve it per ClassifierModel + .view(labels_view) ) + # todo currently assuming class labels are strings ['0.0', '1.0' .. n_classes]. + # need to resolve it per ClassifierModel + data_loader = DataLoader( dataset, batch_size=self.batch_size, diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index d167a39eb..aecab0640 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -24,7 +24,6 @@ from freqtrade.exceptions import OperationalException from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.strategy.interface import IStrategy - logger = logging.getLogger(__name__) @@ -90,8 +89,8 @@ class FreqaiDataDrawer: self.metric_tracker_lock = threading.Lock() self.old_DBSCAN_eps: Dict[str, float] = {} self.empty_pair_dict: pair_info = { - "model_filename": "", "trained_timestamp": 0, - "data_path": "", "extras": {}} + "model_filename": "", "trained_timestamp": 0, + "data_path": "", "extras": {}} self.model_type = self.freqai_info.get('model_save_type', 'joblib') def update_metric_tracker(self, metric: str, value: float, pair: str) -> None: @@ -446,9 +445,9 @@ class FreqaiDataDrawer: dump(model, save_path / f"{dk.model_filename}_model.joblib") elif self.model_type == 'keras': model.save(save_path / f"{dk.model_filename}_model.h5") - elif 'stable_baselines' in self.model_type or\ - 'sb3_contrib' == self.model_type or\ - 'pytorch' == self.model_type: + elif ('stable_baselines' in self.model_type or + 'sb3_contrib' == self.model_type or + 'pytorch' == self.model_type): model.save(save_path / f"{dk.model_filename}_model.zip") if dk.svm_model is not None: @@ -581,16 +580,16 @@ class FreqaiDataDrawer: if len(df_dp.index) == 0: continue if str(hist_df.iloc[-1]["date"]) == str( - df_dp.iloc[-1:]["date"].iloc[-1] + df_dp.iloc[-1:]["date"].iloc[-1] ): continue try: index = ( - df_dp.loc[ - df_dp["date"] == hist_df.iloc[-1]["date"] - ].index[0] - + 1 + df_dp.loc[ + df_dp["date"] == hist_df.iloc[-1]["date"] + ].index[0] + + 1 ) except IndexError: if hist_df.iloc[-1]['date'] < df_dp['date'].iloc[0]: @@ -643,7 +642,7 @@ class FreqaiDataDrawer: ) def get_base_and_corr_dataframes( - self, timerange: TimeRange, pair: str, dk: FreqaiDataKitchen + self, timerange: TimeRange, pair: str, dk: FreqaiDataKitchen ) -> Tuple[Dict[Any, Any], Dict[Any, Any]]: """ Searches through our historic_data in memory and returns the dataframes relevant From 7eedcb9c1475146b0df4b02040c92cadd0575542 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 6 Mar 2023 17:56:07 +0200 Subject: [PATCH 005/208] reformat code --- freqtrade/freqai/data_drawer.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index aecab0640..8d31586fe 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -24,6 +24,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.strategy.interface import IStrategy + logger = logging.getLogger(__name__) @@ -89,8 +90,8 @@ class FreqaiDataDrawer: self.metric_tracker_lock = threading.Lock() self.old_DBSCAN_eps: Dict[str, float] = {} self.empty_pair_dict: pair_info = { - "model_filename": "", "trained_timestamp": 0, - "data_path": "", "extras": {}} + "model_filename": "", "trained_timestamp": 0, + "data_path": "", "extras": {}} self.model_type = self.freqai_info.get('model_save_type', 'joblib') def update_metric_tracker(self, metric: str, value: float, pair: str) -> None: @@ -580,16 +581,16 @@ class FreqaiDataDrawer: if len(df_dp.index) == 0: continue if str(hist_df.iloc[-1]["date"]) == str( - df_dp.iloc[-1:]["date"].iloc[-1] + df_dp.iloc[-1:]["date"].iloc[-1] ): continue try: index = ( - df_dp.loc[ - df_dp["date"] == hist_df.iloc[-1]["date"] - ].index[0] - + 1 + df_dp.loc[ + df_dp["date"] == hist_df.iloc[-1]["date"] + ].index[0] + + 1 ) except IndexError: if hist_df.iloc[-1]['date'] < df_dp['date'].iloc[0]: @@ -642,7 +643,7 @@ class FreqaiDataDrawer: ) def get_base_and_corr_dataframes( - self, timerange: TimeRange, pair: str, dk: FreqaiDataKitchen + self, timerange: TimeRange, pair: str, dk: FreqaiDataKitchen ) -> Tuple[Dict[Any, Any], Dict[Any, Any]]: """ Searches through our historic_data in memory and returns the dataframes relevant From 125085fbaf38713e11a4657a20b2cc7833553ed9 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 6 Mar 2023 18:10:49 +0200 Subject: [PATCH 006/208] add freqai.model_exists pytorch file type support --- freqtrade/freqai/freqai_interface.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 884849446..79bd7d672 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -563,8 +563,11 @@ class IFreqaiModel(ABC): file_type = ".joblib" elif self.dd.model_type == 'keras': file_type = ".h5" - elif 'stable_baselines' in self.dd.model_type or 'sb3_contrib' == self.dd.model_type: + elif ('stable_baselines' in self.dd.model_type or + 'sb3_contrib' == self.dd.model_type or + 'pytorch' == self.dd.model_type): file_type = ".zip" + path_to_modelfile = Path(dk.data_path / f"{dk.model_filename}_model{file_type}") file_exists = path_to_modelfile.is_file() if file_exists: From 8acdd0b47c8cb7239933653b393460a267f39501 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 6 Mar 2023 19:14:54 +0200 Subject: [PATCH 007/208] type hints fixes --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 2 +- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 992ad37ef..52fb0ceb5 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -51,7 +51,7 @@ class PyTorchModelTrainer: # training for batch_data in data_loaders_dictionary['train']: xb, yb = batch_data - xb = xb.to(self.device) # type: ignore + xb = xb.to(self.device) yb = yb.to(self.device) yb_pred = self.model(xb) loss = self.criterion(yb_pred, yb) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 4e1cc32ba..9bbf95019 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -3,6 +3,7 @@ import logging import torch import torch.nn as nn +from torch import Tensor logger = logging.getLogger(__name__) @@ -16,7 +17,7 @@ class PyTorchMLPModel(nn.Module): self.relu = nn.ReLU() self.dropout = nn.Dropout(p=0.2) - def forward(self, x: torch.tensor) -> torch.tensor: + def forward(self, x: Tensor) -> Tensor: x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.relu(self.hidden_layer(x)) From 5dd60eda3693df999f80305cb60f2c6fc3b22a3d Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 6 Mar 2023 19:37:08 +0200 Subject: [PATCH 008/208] type hints fixes --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 52fb0ceb5..02ff35085 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -84,7 +84,7 @@ class PyTorchModelTrainer: loss = self.criterion(yb_pred, yb) losses[i] = loss.item() - loss_dictionary[split] = losses.mean() + loss_dictionary[split] = losses.mean().item() self.model.train() return loss_dictionary From 4241bff32aee728f4d2b57f52c667e15ece3e33c Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 6 Mar 2023 20:15:36 +0200 Subject: [PATCH 009/208] type hints fixes --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 02ff35085..fc0a7600e 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -1,6 +1,7 @@ import logging from pathlib import Path from typing import Dict +from torch.optim import Optimizer import torch import torch.nn as nn @@ -15,7 +16,7 @@ class PyTorchModelTrainer: def __init__( self, model: nn.Module, - optimizer: nn.Module, + optimizer: Optimizer, criterion: nn.Module, device: str, batch_size: int, From 76fbec0c175714148cead01a6c85f36d91de3244 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 14:29:38 +0200 Subject: [PATCH 010/208] ad multiclass target names encoder to ints --- config_examples/config_freqai.example.json | 3 +- .../PyTorchClassifierMultiTarget.py | 63 +++++++++++++++---- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/config_examples/config_freqai.example.json b/config_examples/config_freqai.example.json index 65a93379e..479e94aa3 100644 --- a/config_examples/config_freqai.example.json +++ b/config_examples/config_freqai.example.json @@ -79,7 +79,8 @@ "test_size": 0.33, "random_state": 1 }, - "model_training_parameters": {} + "model_training_parameters": {}, + "multiclass_target_names": ["down", "neither", "up"] }, "bot_name": "", "force_entry_enable": true, diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index 9504fffb8..aead0e46c 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -1,6 +1,6 @@ import logging -from typing import Any, Dict, Tuple +from typing import Any, Dict, Tuple, List import numpy.typing as npt import numpy as np @@ -9,6 +9,7 @@ import torch from pandas import DataFrame from torch.nn import functional as F +from freqtrade.exceptions import OperationalException from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel @@ -23,13 +24,23 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): def __init__(self, **kwargs): super().__init__(**kwargs) - # todo move to config - self.labels = ['0.0', '1.0', '2.0'] - self.n_hidden = 1024 - self.max_iters = 100 - self.batch_size = 64 - self.learning_rate = 3e-4 - self.eval_iters = 10 + self.multiclass_names = self.freqai_info["multiclass_target_names"] + if not self.multiclass_names: + raise OperationalException( + "Missing 'multiclass_names' in freqai_info," + " multi class pytorch model requires predefined list of" + " class names matching the strategy being used" + ) + + self.class_name_to_index = {s: i for i, s in enumerate(self.multiclass_names)} + self.index_to_class_name = {i: s for i, s in enumerate(self.multiclass_names)} + + model_training_parameters = self.freqai_info["model_training_parameters"] + self.n_hidden = model_training_parameters.get("n_hidden", 1024) + self.max_iters = model_training_parameters.get("max_iters", 100) + self.batch_size = model_training_parameters.get("batch_size", 64) + self.learning_rate = model_training_parameters.get("learning_rate", 3e-4) + self.eval_iters = model_training_parameters.get("eval_iters", 10) def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ @@ -37,12 +48,13 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): :param tensor_dictionary: the dictionary constructed by DataHandler to hold all the training and test data/labels. """ - n_features = data_dictionary['train_features'].shape[-1] + self.encode_classes_name(data_dictionary, dk) + n_features = data_dictionary['train_features'].shape[-1] model = PyTorchMLPModel( input_dim=n_features, hidden_dim=self.n_hidden, - output_dim=len(self.labels) + output_dim=len(self.multiclass_names) ) model.to(self.device) optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) @@ -87,9 +99,34 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): logits = self.model.model(dk.data_dictionary["prediction_features"]) probs = F.softmax(logits, dim=-1) - label_ints = torch.argmax(probs, dim=-1) + predicted_classes = torch.argmax(probs, dim=-1) + predicted_classes_str = self.decode_classes_name(predicted_classes) - pred_df_prob = DataFrame(probs.detach().numpy(), columns=self.labels) - pred_df = DataFrame(label_ints, columns=dk.label_list).astype(float).astype(str) + pred_df_prob = DataFrame(probs.detach().numpy(), columns=self.multiclass_names) + pred_df = DataFrame(predicted_classes_str, columns=[dk.label_list[0]]) pred_df = pd.concat([pred_df, pred_df_prob], axis=1) return (pred_df, dk.do_predict) + + def encode_classes_name(self, data_dictionary: Dict[str, pd.DataFrame], dk: FreqaiDataKitchen): + """ + encode class name str -> int + assuming first column of *_labels data frame to contain class names + """ + target_column_name = dk.label_list[0] + for split in ["train", "test"]: + label_df = data_dictionary[f"{split}_labels"] + self.assert_valid_class_names(label_df[target_column_name]) + label_df[target_column_name] = list( + map(lambda x: self.class_name_to_index[x], label_df[target_column_name]) + ) + + def assert_valid_class_names(self, labels: pd.Series): + non_defined_labels = set(labels) - set(self.multiclass_names) + if len(non_defined_labels) != 0: + raise OperationalException( + f"Found non defined labels {non_defined_labels} ", + f"expecting labels {self.multiclass_names}" + ) + + def decode_classes_name(self, classes: List[int]) -> List[str]: + return list(map(lambda x: self.index_to_class_name[x], classes)) \ No newline at end of file From 1805db2b077157e65c78543a91ce1e1f8eb09fdc Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 15:38:22 +0200 Subject: [PATCH 011/208] change documentation and small bugfix --- .../freqai/base_models/PyTorchModelTrainer.py | 2 -- .../PyTorchClassifierMultiTarget.py | 22 +++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index fc0a7600e..d02f1d896 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -104,8 +104,6 @@ class PyTorchModelTrainer: .long() .view(labels_view) ) - # todo currently assuming class labels are strings ['0.0', '1.0' .. n_classes]. - # need to resolve it per ClassifierModel data_loader = DataLoader( dataset, diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index aead0e46c..e58fa9cff 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -24,16 +24,18 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): def __init__(self, **kwargs): super().__init__(**kwargs) - self.multiclass_names = self.freqai_info["multiclass_target_names"] + self.multiclass_names = self.freqai_info.get("multiclass_target_names", None) + logger.info(f"setting multiclass_names: {self.multiclass_names}") if not self.multiclass_names: raise OperationalException( - "Missing 'multiclass_names' in freqai_info," - " multi class pytorch model requires predefined list of" - " class names matching the strategy being used" + "Missing 'multiclass_names' in freqai_info, " + "multi class pytorch classifier model requires predefined list of " + "class names matching the strategy being used." ) self.class_name_to_index = {s: i for i, s in enumerate(self.multiclass_names)} self.index_to_class_name = {i: s for i, s in enumerate(self.multiclass_names)} + logger.info(f"class_name_to_index: {self.class_name_to_index}") model_training_parameters = self.freqai_info["model_training_parameters"] self.n_hidden = model_training_parameters.get("n_hidden", 1024) @@ -48,7 +50,6 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): :param tensor_dictionary: the dictionary constructed by DataHandler to hold all the training and test data/labels. """ - self.encode_classes_name(data_dictionary, dk) n_features = data_dictionary['train_features'].shape[-1] model = PyTorchMLPModel( @@ -124,9 +125,12 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): non_defined_labels = set(labels) - set(self.multiclass_names) if len(non_defined_labels) != 0: raise OperationalException( - f"Found non defined labels {non_defined_labels} ", - f"expecting labels {self.multiclass_names}" + f"Found non defined labels: {non_defined_labels}, ", + f"expecting labels: {self.multiclass_names}" ) - def decode_classes_name(self, classes: List[int]) -> List[str]: - return list(map(lambda x: self.index_to_class_name[x], classes)) \ No newline at end of file + def decode_classes_name(self, classes: torch.Tensor[int]) -> List[str]: + """ + decode class name int -> str + """ + return list(map(lambda x: self.index_to_class_name[x.item()], classes)) From dfbb2e2b35e79f023bdfb98ca8a8e840ea4fae8d Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 16:03:36 +0200 Subject: [PATCH 012/208] sort imports --- freqtrade/freqai/base_models/BasePyTorchModel.py | 1 + freqtrade/freqai/base_models/PyTorchModelTrainer.py | 8 ++++---- .../prediction_models/PyTorchClassifierMultiTarget.py | 9 +++------ freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 5 ++--- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/freqtrade/freqai/base_models/BasePyTorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py index 1074ddeea..efc36fdec 100644 --- a/freqtrade/freqai/base_models/BasePyTorchModel.py +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -8,6 +8,7 @@ from pandas import DataFrame from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.freqai_interface import IFreqaiModel + logger = logging.getLogger(__name__) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index d02f1d896..464c5dc43 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -1,13 +1,13 @@ import logging from pathlib import Path from typing import Dict -from torch.optim import Optimizer +import pandas as pd import torch import torch.nn as nn -from torch.utils.data import DataLoader -from torch.utils.data import TensorDataset -import pandas as pd +from torch.optim import Optimizer +from torch.utils.data import DataLoader, TensorDataset + logger = logging.getLogger(__name__) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index e58fa9cff..3623728db 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -1,19 +1,16 @@ import logging - -from typing import Any, Dict, Tuple, List -import numpy.typing as npt +from typing import Any, Dict, Tuple import numpy as np +import numpy.typing as npt import pandas as pd import torch from pandas import DataFrame from torch.nn import functional as F -from freqtrade.exceptions import OperationalException -from freqtrade.freqai.data_kitchen import FreqaiDataKitchen - from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 9bbf95019..88599acb6 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -1,9 +1,8 @@ import logging - -import torch -import torch.nn as nn from torch import Tensor +import torch.nn as nn + logger = logging.getLogger(__name__) From b65ade51bed5c2bf5c4113b9f1c90c20d55a330a Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 16:05:02 +0200 Subject: [PATCH 013/208] revert config_freqai_example changes --- config_examples/config_freqai.example.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config_examples/config_freqai.example.json b/config_examples/config_freqai.example.json index 479e94aa3..65a93379e 100644 --- a/config_examples/config_freqai.example.json +++ b/config_examples/config_freqai.example.json @@ -79,8 +79,7 @@ "test_size": 0.33, "random_state": 1 }, - "model_training_parameters": {}, - "multiclass_target_names": ["down", "neither", "up"] + "model_training_parameters": {} }, "bot_name": "", "force_entry_enable": true, From 1921a07b8912a447f0fa8eed87f6f22caafdafa1 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 16:08:04 +0200 Subject: [PATCH 014/208] sort imports --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 88599acb6..de9c25293 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -1,8 +1,7 @@ import logging -from torch import Tensor import torch.nn as nn - +from torch import Tensor logger = logging.getLogger(__name__) From 6161b858c44a3f278a81c648661e0d722be03a6d Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 16:10:25 +0200 Subject: [PATCH 015/208] sort imports --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index de9c25293..1f13ca069 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -3,6 +3,7 @@ import logging import torch.nn as nn from torch import Tensor + logger = logging.getLogger(__name__) From 04564dc134bae650ba7fed16bdf01c5309ddb2b3 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 16:11:51 +0200 Subject: [PATCH 016/208] add missing import --- .../freqai/prediction_models/PyTorchClassifierMultiTarget.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index 3623728db..ae8728490 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -8,6 +8,7 @@ import torch from pandas import DataFrame from torch.nn import functional as F +from freqtrade.exceptions import OperationalException from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen From 8d60327d60e08a78168342e8fd2ee18ecb01c547 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 16:12:47 +0200 Subject: [PATCH 017/208] add missing import --- .../freqai/prediction_models/PyTorchClassifierMultiTarget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index ae8728490..cfcf57364 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, Tuple +from typing import Any, Dict, Tuple, List import numpy as np import numpy.typing as npt From c8296ccb2d289b68171924bb61af96954b26effc Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 16:13:35 +0200 Subject: [PATCH 018/208] sort imports --- .../freqai/prediction_models/PyTorchClassifierMultiTarget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index cfcf57364..62bec0fd9 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, Tuple, List +from typing import Any, Dict, List, Tuple import numpy as np import numpy.typing as npt From 7d26df01b814bcd0b9612740a976cfc74e284618 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 16:16:49 +0200 Subject: [PATCH 019/208] fix tensor type hint --- .../freqai/prediction_models/PyTorchClassifierMultiTarget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index 62bec0fd9..f33248e7d 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -127,7 +127,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): f"expecting labels: {self.multiclass_names}" ) - def decode_classes_name(self, classes: torch.Tensor[int]) -> List[str]: + def decode_classes_name(self, classes: torch.Tensor) -> List[str]: """ decode class name int -> str """ From 1597c3aa89f2425f7ec076520a837b7582844a54 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 18:36:44 +0200 Subject: [PATCH 020/208] set class names in IStrategy.set_freqai_targets method, also save class name with model meta data --- .../freqai/base_models/PyTorchModelTrainer.py | 8 +++- .../PyTorchClassifierMultiTarget.py | 45 +++++++++++-------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 464c5dc43..5ebecef34 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Dict +from typing import Any, Dict import pandas as pd import torch @@ -22,11 +22,13 @@ class PyTorchModelTrainer: batch_size: int, max_iters: int, eval_iters: int, - init_model: Dict + init_model: Dict, + model_meta_data: Dict[str, Any] = {}, ): self.model = model self.optimizer = optimizer self.criterion = criterion + self.model_meta_data = model_meta_data self.device = device self.max_iters = max_iters self.batch_size = batch_size @@ -126,6 +128,7 @@ class PyTorchModelTrainer: torch.save({ 'model_state_dict': self.model.state_dict(), 'optimizer_state_dict': self.optimizer.state_dict(), + 'model_meta_data': self.model_meta_data, }, path) def load_from_file(self, path: Path): @@ -135,4 +138,5 @@ class PyTorchModelTrainer: def load_from_checkpoint(self, checkpoint: Dict): self.model.load_state_dict(checkpoint['model_state_dict']) self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) + self.model_meta_data = checkpoint["model_meta_data"] return self diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index f33248e7d..13ec2d0bb 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -22,25 +22,14 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): def __init__(self, **kwargs): super().__init__(**kwargs) - self.multiclass_names = self.freqai_info.get("multiclass_target_names", None) - logger.info(f"setting multiclass_names: {self.multiclass_names}") - if not self.multiclass_names: - raise OperationalException( - "Missing 'multiclass_names' in freqai_info, " - "multi class pytorch classifier model requires predefined list of " - "class names matching the strategy being used." - ) - - self.class_name_to_index = {s: i for i, s in enumerate(self.multiclass_names)} - self.index_to_class_name = {i: s for i, s in enumerate(self.multiclass_names)} - logger.info(f"class_name_to_index: {self.class_name_to_index}") - model_training_parameters = self.freqai_info["model_training_parameters"] self.n_hidden = model_training_parameters.get("n_hidden", 1024) self.max_iters = model_training_parameters.get("max_iters", 100) self.batch_size = model_training_parameters.get("batch_size", 64) self.learning_rate = model_training_parameters.get("learning_rate", 3e-4) self.eval_iters = model_training_parameters.get("eval_iters", 10) + self.class_name_to_index = None + self.index_to_class_name = None def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ @@ -48,12 +37,20 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): :param tensor_dictionary: the dictionary constructed by DataHandler to hold all the training and test data/labels. """ + if not hasattr(self, "class_names"): + raise ValueError( + "Missing attribute: self.class_names " + "set self.freqai.class_names = [\"class a\", \"class b\", \"class c\"] " + "inside IStrategy.set_freqai_targets method." + ) + + self.init_class_names_to_index_mapping(self.class_names) self.encode_classes_name(data_dictionary, dk) n_features = data_dictionary['train_features'].shape[-1] model = PyTorchMLPModel( input_dim=n_features, hidden_dim=self.n_hidden, - output_dim=len(self.multiclass_names) + output_dim=len(self.class_names) ) model.to(self.device) optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) @@ -63,6 +60,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): model=model, optimizer=optimizer, criterion=criterion, + model_meta_data={"class_names": self.class_names}, device=self.device, batch_size=self.batch_size, max_iters=self.max_iters, @@ -83,6 +81,13 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove data (NaNs) or felt uncertain about data (PCA and DI index) """ + class_names = self.model.model_meta_data.get("class_names", None) + if not class_names: + raise ValueError( + "Missing class names. " + "self.model.model_meta_data[\"class_names\"] is None." + ) + self.init_class_names_to_index_mapping(class_names) dk.find_features(unfiltered_df) filtered_df, _ = dk.filter_features( @@ -100,8 +105,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): probs = F.softmax(logits, dim=-1) predicted_classes = torch.argmax(probs, dim=-1) predicted_classes_str = self.decode_classes_name(predicted_classes) - - pred_df_prob = DataFrame(probs.detach().numpy(), columns=self.multiclass_names) + pred_df_prob = DataFrame(probs.detach().numpy(), columns=class_names) pred_df = DataFrame(predicted_classes_str, columns=[dk.label_list[0]]) pred_df = pd.concat([pred_df, pred_df_prob], axis=1) return (pred_df, dk.do_predict) @@ -120,11 +124,11 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): ) def assert_valid_class_names(self, labels: pd.Series): - non_defined_labels = set(labels) - set(self.multiclass_names) + non_defined_labels = set(labels) - set(self.class_names) if len(non_defined_labels) != 0: raise OperationalException( f"Found non defined labels: {non_defined_labels}, ", - f"expecting labels: {self.multiclass_names}" + f"expecting labels: {self.class_names}" ) def decode_classes_name(self, classes: torch.Tensor) -> List[str]: @@ -132,3 +136,8 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): decode class name int -> str """ return list(map(lambda x: self.index_to_class_name[x.item()], classes)) + + def init_class_names_to_index_mapping(self, class_names): + self.class_name_to_index = {s: i for i, s in enumerate(class_names)} + self.index_to_class_name = {i: s for i, s in enumerate(class_names)} + logger.info(f"class_name_to_index: {self.class_name_to_index}") From 3081b9402b78406c4edee6e6ef6cdc4937b64e50 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 9 Mar 2023 11:14:54 +0200 Subject: [PATCH 021/208] add documentation --- .../freqai/base_models/BasePyTorchModel.py | 2 +- .../freqai/base_models/PyTorchModelTrainer.py | 21 +++++++++++++++++++ .../PyTorchClassifierMultiTarget.py | 17 +++++++++++++-- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqai/base_models/BasePyTorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py index efc36fdec..8e608ee1a 100644 --- a/freqtrade/freqai/base_models/BasePyTorchModel.py +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) class BasePyTorchModel(IFreqaiModel): """ - Base class for TensorFlow type models. + Base class for PyTorch type models. User *must* inherit from this class and set fit() and predict(). """ diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 5ebecef34..26149e2fa 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -25,6 +25,21 @@ class PyTorchModelTrainer: init_model: Dict, model_meta_data: Dict[str, Any] = {}, ): + """ + A class for training PyTorch models. + + :param model: The PyTorch model to be trained. + :param optimizer: The optimizer to use for training. + :param criterion: The loss function to use for training. + :param device: The device to use for training (e.g. 'cpu', 'cuda'). + :param batch_size: The size of the batches to use during training. + :param max_iters: The number of training iterations to run. + iteration here refers to the number of times we call + self.optimizer.step() . used to calculate n_epochs. + :param eval_iters: The number of iterations used to estimate the loss. + :param init_model: A dictionary containing the initial model parameters. + :param model_meta_data: Additional metadata about the model (optional). + """ self.model = model self.optimizer = optimizer self.criterion = criterion @@ -38,6 +53,12 @@ class PyTorchModelTrainer: self.load_from_checkpoint(init_model) def fit(self, data_dictionary: Dict[str, pd.DataFrame]): + """ + general training loop: + - converting data to tensors + - calculating n_epochs + - + """ data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary) epochs = self.calc_n_epochs( n_obs=len(data_dictionary['train_features']), diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index 13ec2d0bb..e8326ffe9 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -19,8 +19,19 @@ logger = logging.getLogger(__name__) class PyTorchClassifierMultiTarget(BasePyTorchModel): - + """ + A PyTorch implementation of a multi-target classifier. + """ def __init__(self, **kwargs): + """ + int: The number of nodes in the hidden layer of the neural network. + int: The maximum number of iterations to run during training. + int: The batch size to use during training. + float: The learning rate to use during training. + int: The number of training iterations between each evaluation. + dict: A dictionary mapping class names to their corresponding indices. + dict: A dictionary mapping indices to their corresponding class names. + """ super().__init__(**kwargs) model_training_parameters = self.freqai_info["model_training_parameters"] self.n_hidden = model_training_parameters.get("n_hidden", 1024) @@ -34,8 +45,10 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param tensor_dictionary: the dictionary constructed by DataHandler to hold + :param data_dictionary: the dictionary constructed by DataHandler to hold all the training and test data/labels. + :raises ValueError: If self.class_names is not defined in the parent class. + """ if not hasattr(self, "class_names"): raise ValueError( From ba5de0cd00423570cc484a44ffa281718bda47a9 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 9 Mar 2023 11:21:10 +0200 Subject: [PATCH 022/208] add documentation --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 26149e2fa..41d26e31a 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -54,10 +54,7 @@ class PyTorchModelTrainer: def fit(self, data_dictionary: Dict[str, pd.DataFrame]): """ - general training loop: - - converting data to tensors - - calculating n_epochs - - + General training loop. """ data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary) epochs = self.calc_n_epochs( @@ -117,6 +114,9 @@ class PyTorchModelTrainer: self, data_dictionary: Dict[str, pd.DataFrame] ) -> Dict[str, DataLoader]: + """ + Converts the input data to PyTorch tensors using a data loader. + """ data_loader_dictionary = {} for split in ['train', 'test']: labels_shape = data_dictionary[f'{split}_labels'].shape @@ -141,6 +141,10 @@ class PyTorchModelTrainer: @staticmethod def calc_n_epochs(n_obs: int, batch_size: int, n_iters: int) -> int: + """ + Calculates the number of epochs required to reach the maximum number + of iterations specified in the model training parameters. + """ n_batches = n_obs // batch_size epochs = n_iters // n_batches return epochs From 6f962362f2dc4f9e3959ede2df045fbc144b8412 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 9 Mar 2023 12:45:46 +0200 Subject: [PATCH 023/208] expand pytorch trainer documentation --- .../freqai/base_models/PyTorchModelTrainer.py | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 41d26e31a..525d543af 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -27,6 +27,27 @@ class PyTorchModelTrainer: ): """ A class for training PyTorch models. + Implements the training loop logic, load/save methods. + + fit method - training loop logic: + - Calculates the predicted output for the batch using the PyTorch model. + - Calculates the loss between the predicted and actual output using a loss function. + - Computes the gradients of the loss with respect to the model's parameters using + backpropagation. + - Updates the model's parameters using an optimizer. + + save method: + called by DataDrawer + - Saving any nn.Module state_dict + - Saving model_meta_data, this dict should contain any additional data that the + user needs to store. e.g class_names for classification models. + + load method: + currently DataDrawer is responsible for the actual loading. + when using continual_learning the DataDrawer will load the dict + (saved by self.save(path)). and this class will populate the necessary + state_dict of the self.model & self.optimizer and self.model_meta_data. + :param model: The PyTorch model to be trained. :param optimizer: The optimizer to use for training. @@ -34,10 +55,11 @@ class PyTorchModelTrainer: :param device: The device to use for training (e.g. 'cpu', 'cuda'). :param batch_size: The size of the batches to use during training. :param max_iters: The number of training iterations to run. - iteration here refers to the number of times we call - self.optimizer.step() . used to calculate n_epochs. + iteration here refers to the number of times we call self.optimizer.step(). + used to calculate n_epochs. :param eval_iters: The number of iterations used to estimate the loss. - :param init_model: A dictionary containing the initial model parameters. + :param init_model: A dictionary containing the initial model/optimizer + state_dict and model_meta_data saved by self.save() method. :param model_meta_data: Additional metadata about the model (optional). """ self.model = model From c9eee2944b2b498557d4e5a44028812b8a350920 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 9 Mar 2023 13:01:04 +0200 Subject: [PATCH 024/208] reformat documentation --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 525d543af..4a091f52c 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -55,8 +55,8 @@ class PyTorchModelTrainer: :param device: The device to use for training (e.g. 'cpu', 'cuda'). :param batch_size: The size of the batches to use during training. :param max_iters: The number of training iterations to run. - iteration here refers to the number of times we call self.optimizer.step(). - used to calculate n_epochs. + iteration here refers to the number of times we call + self.optimizer.step(). used to calculate n_epochs. :param eval_iters: The number of iterations used to estimate the loss. :param init_model: A dictionary containing the initial model/optimizer state_dict and model_meta_data saved by self.save() method. From 2ef11faba7cd7fc89805a10edfe735e81bc8aca2 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 9 Mar 2023 13:25:20 +0200 Subject: [PATCH 025/208] reformat documentation --- .../freqai/base_models/PyTorchModelTrainer.py | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 4a091f52c..a934814ef 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -26,29 +26,6 @@ class PyTorchModelTrainer: model_meta_data: Dict[str, Any] = {}, ): """ - A class for training PyTorch models. - Implements the training loop logic, load/save methods. - - fit method - training loop logic: - - Calculates the predicted output for the batch using the PyTorch model. - - Calculates the loss between the predicted and actual output using a loss function. - - Computes the gradients of the loss with respect to the model's parameters using - backpropagation. - - Updates the model's parameters using an optimizer. - - save method: - called by DataDrawer - - Saving any nn.Module state_dict - - Saving model_meta_data, this dict should contain any additional data that the - user needs to store. e.g class_names for classification models. - - load method: - currently DataDrawer is responsible for the actual loading. - when using continual_learning the DataDrawer will load the dict - (saved by self.save(path)). and this class will populate the necessary - state_dict of the self.model & self.optimizer and self.model_meta_data. - - :param model: The PyTorch model to be trained. :param optimizer: The optimizer to use for training. :param criterion: The loss function to use for training. @@ -76,7 +53,11 @@ class PyTorchModelTrainer: def fit(self, data_dictionary: Dict[str, pd.DataFrame]): """ - General training loop. + - Calculates the predicted output for the batch using the PyTorch model. + - Calculates the loss between the predicted and actual output using a loss function. + - Computes the gradients of the loss with respect to the model's parameters using + backpropagation. + - Updates the model's parameters using an optimizer. """ data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary) epochs = self.calc_n_epochs( @@ -172,6 +153,12 @@ class PyTorchModelTrainer: return epochs def save(self, path: Path): + """ + - Saving any nn.Module state_dict + - Saving model_meta_data, this dict should contain any additional data that the + user needs to store. e.g class_names for classification models. + """ + torch.save({ 'model_state_dict': self.model.state_dict(), 'optimizer_state_dict': self.optimizer.state_dict(), @@ -183,7 +170,14 @@ class PyTorchModelTrainer: return self.load_from_checkpoint(checkpoint) def load_from_checkpoint(self, checkpoint: Dict): - self.model.load_state_dict(checkpoint['model_state_dict']) - self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) + """ + when using continual_learning, DataDrawer will load the dictionary + (containing state dicts and model_meta_data) by calling torch.load(path). + you can access this dict from any class that inherits IFreqaiModel by calling + get_init_model method. + """ + + self.model.load_state_dict(checkpoint["model_state_dict"]) + self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"]) self.model_meta_data = checkpoint["model_meta_data"] return self From e88a0d52482ed8f7d955eaae7055a90d76543c3a Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 9 Mar 2023 13:29:11 +0200 Subject: [PATCH 026/208] convert single quotes to double quotes --- .../freqai/base_models/BasePyTorchModel.py | 6 ++--- .../freqai/base_models/PyTorchModelTrainer.py | 23 ++++++++++--------- .../PyTorchClassifierMultiTarget.py | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/freqtrade/freqai/base_models/BasePyTorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py index 8e608ee1a..d6372fa36 100644 --- a/freqtrade/freqai/base_models/BasePyTorchModel.py +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -19,9 +19,9 @@ class BasePyTorchModel(IFreqaiModel): """ def __init__(self, **kwargs): - super().__init__(config=kwargs['config']) - self.dd.model_type = 'pytorch' - self.device = 'cuda' if torch.cuda.is_available() else 'cpu' + super().__init__(config=kwargs["config"]) + self.dd.model_type = "pytorch" + self.device = "cuda" if torch.cuda.is_available() else "cpu" def train( self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index a934814ef..0ca28d2e9 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -61,7 +61,7 @@ class PyTorchModelTrainer: """ data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary) epochs = self.calc_n_epochs( - n_obs=len(data_dictionary['train_features']), + n_obs=len(data_dictionary["train_features"]), batch_size=self.batch_size, n_iters=self.max_iters ) @@ -73,7 +73,7 @@ class PyTorchModelTrainer: f" train loss {losses['train']:.4f} ; test loss {losses['test']:.4f}" ) # training - for batch_data in data_loaders_dictionary['train']: + for batch_data in data_loaders_dictionary["train"]: xb, yb = batch_data xb = xb.to(self.device) yb = yb.to(self.device) @@ -93,12 +93,12 @@ class PyTorchModelTrainer: self.model.eval() epochs = self.calc_n_epochs( - n_obs=len(data_dictionary['test_features']), + n_obs=len(data_dictionary["test_features"]), batch_size=self.batch_size, n_iters=self.eval_iters ) loss_dictionary = {} - for split in ['train', 'test']: + for split in ["train", "test"]: losses = torch.zeros(epochs) for i, batch in enumerate(data_loader_dictionary[split]): xb, yb = batch @@ -121,12 +121,12 @@ class PyTorchModelTrainer: Converts the input data to PyTorch tensors using a data loader. """ data_loader_dictionary = {} - for split in ['train', 'test']: - labels_shape = data_dictionary[f'{split}_labels'].shape + for split in ["train", "test"]: + labels_shape = data_dictionary[f"{split}_labels"].shape labels_view = labels_shape[0] if labels_shape[1] == 1 else labels_shape dataset = TensorDataset( - torch.from_numpy(data_dictionary[f'{split}_features'].values).float(), - torch.from_numpy(data_dictionary[f'{split}_labels'].astype(float).values) + torch.from_numpy(data_dictionary[f"{split}_features"].values).float(), + torch.from_numpy(data_dictionary[f"{split}_labels"].astype(float).values) .long() .view(labels_view) ) @@ -148,6 +148,7 @@ class PyTorchModelTrainer: Calculates the number of epochs required to reach the maximum number of iterations specified in the model training parameters. """ + n_batches = n_obs // batch_size epochs = n_iters // n_batches return epochs @@ -160,9 +161,9 @@ class PyTorchModelTrainer: """ torch.save({ - 'model_state_dict': self.model.state_dict(), - 'optimizer_state_dict': self.optimizer.state_dict(), - 'model_meta_data': self.model_meta_data, + "model_state_dict": self.model.state_dict(), + "optimizer_state_dict": self.optimizer.state_dict(), + "model_meta_data": self.model_meta_data, }, path) def load_from_file(self, path: Path): diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index e8326ffe9..a98643b3f 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -59,7 +59,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): self.init_class_names_to_index_mapping(self.class_names) self.encode_classes_name(data_dictionary, dk) - n_features = data_dictionary['train_features'].shape[-1] + n_features = data_dictionary["train_features"].shape[-1] model = PyTorchMLPModel( input_dim=n_features, hidden_dim=self.n_hidden, From 8a9f2aedbbfa9e4019f132751c23d48f670651b0 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 9 Mar 2023 14:55:52 +0200 Subject: [PATCH 027/208] improve documentation --- .../prediction_models/PyTorchClassifierMultiTarget.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index a98643b3f..a5b8b1591 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -32,6 +32,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): dict: A dictionary mapping class names to their corresponding indices. dict: A dictionary mapping indices to their corresponding class names. """ + super().__init__(**kwargs) model_training_parameters = self.freqai_info["model_training_parameters"] self.n_hidden = model_training_parameters.get("n_hidden", 1024) @@ -50,6 +51,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): :raises ValueError: If self.class_names is not defined in the parent class. """ + if not hasattr(self, "class_names"): raise ValueError( "Missing attribute: self.class_names " @@ -93,7 +95,9 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): :pred_df: dataframe containing the predictions :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove data (NaNs) or felt uncertain about data (PCA and DI index) + :raises ValueError: if 'class_name' doesn't exist in model meta_data. """ + class_names = self.model.model_meta_data.get("class_names", None) if not class_names: raise ValueError( @@ -128,6 +132,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): encode class name str -> int assuming first column of *_labels data frame to contain class names """ + target_column_name = dk.label_list[0] for split in ["train", "test"]: label_df = data_dictionary[f"{split}_labels"] @@ -148,6 +153,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): """ decode class name int -> str """ + return list(map(lambda x: self.index_to_class_name[x.item()], classes)) def init_class_names_to_index_mapping(self, class_names): From 1cf0e7be2495bd879025b44e19afddeb2c7d3508 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 12 Mar 2023 12:48:15 +0200 Subject: [PATCH 028/208] use one iteration on all test and train data for evaluation --- .../freqai/base_models/PyTorchModelTrainer.py | 28 +++++++++---------- .../PyTorchClassifierMultiTarget.py | 4 +-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 0ca28d2e9..99ee44e3b 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, Optional import pandas as pd import torch @@ -21,7 +21,7 @@ class PyTorchModelTrainer: device: str, batch_size: int, max_iters: int, - eval_iters: int, + max_n_eval_batches: int, init_model: Dict, model_meta_data: Dict[str, Any] = {}, ): @@ -34,7 +34,7 @@ class PyTorchModelTrainer: :param max_iters: The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs. - :param eval_iters: The number of iterations used to estimate the loss. + :param max_n_eval_batches: The maximum number batches to use for evaluation. :param init_model: A dictionary containing the initial model/optimizer state_dict and model_meta_data saved by self.save() method. :param model_meta_data: Additional metadata about the model (optional). @@ -46,7 +46,7 @@ class PyTorchModelTrainer: self.device = device self.max_iters = max_iters self.batch_size = batch_size - self.eval_iters = eval_iters + self.max_n_eval_batches = max_n_eval_batches if init_model: self.load_from_checkpoint(init_model) @@ -67,7 +67,7 @@ class PyTorchModelTrainer: ) for epoch in range(epochs): # evaluation - losses = self.estimate_loss(data_loaders_dictionary, data_dictionary) + losses = self.estimate_loss(data_loaders_dictionary, self.max_n_eval_batches) logger.info( f"epoch ({epoch}/{epochs}):" f" train loss {losses['train']:.4f} ; test loss {losses['test']:.4f}" @@ -88,27 +88,27 @@ class PyTorchModelTrainer: def estimate_loss( self, data_loader_dictionary: Dict[str, DataLoader], - data_dictionary: Dict[str, pd.DataFrame] + max_n_eval_batches: Optional[int] ) -> Dict[str, float]: self.model.eval() - epochs = self.calc_n_epochs( - n_obs=len(data_dictionary["test_features"]), - batch_size=self.batch_size, - n_iters=self.eval_iters - ) loss_dictionary = {} + n_batches = 0 for split in ["train", "test"]: - losses = torch.zeros(epochs) + losses = [] for i, batch in enumerate(data_loader_dictionary[split]): + if max_n_eval_batches and i > max_n_eval_batches: + n_batches += 1 + break + xb, yb = batch xb = xb.to(self.device) yb = yb.to(self.device) yb_pred = self.model(xb) loss = self.criterion(yb_pred, yb) - losses[i] = loss.item() + losses.append(loss.item()) - loss_dictionary[split] = losses.mean().item() + loss_dictionary[split] = sum(losses) / len(losses) self.model.train() return loss_dictionary diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index a5b8b1591..f951778bf 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -39,7 +39,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): self.max_iters = model_training_parameters.get("max_iters", 100) self.batch_size = model_training_parameters.get("batch_size", 64) self.learning_rate = model_training_parameters.get("learning_rate", 3e-4) - self.eval_iters = model_training_parameters.get("eval_iters", 10) + self.max_n_eval_batches = model_training_parameters.get("max_n_eval_batches", None) self.class_name_to_index = None self.index_to_class_name = None @@ -79,7 +79,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): device=self.device, batch_size=self.batch_size, max_iters=self.max_iters, - eval_iters=self.eval_iters, + max_n_eval_batches=self.max_n_eval_batches, init_model=init_model ) trainer.fit(data_dictionary) From f9fdf1c31b7d437f269aed33626d05c6bae6bf10 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 12 Mar 2023 14:31:08 +0200 Subject: [PATCH 029/208] generalize mlp model --- .../PyTorchClassifierMultiTarget.py | 19 +++++----- .../prediction_models/PyTorchMLPModel.py | 38 ++++++++++++++++--- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index f951778bf..be42fd8e6 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Tuple, Optional import numpy as np import numpy.typing as npt @@ -34,12 +34,13 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): """ super().__init__(**kwargs) - model_training_parameters = self.freqai_info["model_training_parameters"] - self.n_hidden = model_training_parameters.get("n_hidden", 1024) - self.max_iters = model_training_parameters.get("max_iters", 100) - self.batch_size = model_training_parameters.get("batch_size", 64) - self.learning_rate = model_training_parameters.get("learning_rate", 3e-4) - self.max_n_eval_batches = model_training_parameters.get("max_n_eval_batches", None) + trainer_kwargs = self.freqai_info.get("trainer_kwargs", {}) + self.n_hidden: int = trainer_kwargs.get("n_hidden", 1024) + self.max_iters: int = trainer_kwargs.get("max_iters", 100) + self.batch_size: int = trainer_kwargs.get("batch_size", 64) + self.learning_rate: float = trainer_kwargs.get("learning_rate", 3e-4) + self.max_n_eval_batches: Optional[int] = trainer_kwargs.get("max_n_eval_batches", None) + self.model_kwargs: Dict = trainer_kwargs.get("model_kwargs", {}) self.class_name_to_index = None self.index_to_class_name = None @@ -64,8 +65,8 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): n_features = data_dictionary["train_features"].shape[-1] model = PyTorchMLPModel( input_dim=n_features, - hidden_dim=self.n_hidden, - output_dim=len(self.class_names) + output_dim=len(self.class_names), + **self.model_kwargs ) model.to(self.device) optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 1f13ca069..91e496c5d 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -8,18 +8,46 @@ logger = logging.getLogger(__name__) class PyTorchMLPModel(nn.Module): - def __init__(self, input_dim: int, hidden_dim: int, output_dim: int): + def __init__(self, input_dim: int, output_dim: int, **kwargs): super(PyTorchMLPModel, self).__init__() + hidden_dim: int = kwargs.get("hidden_dim", 1024) + dropout_percent: int = kwargs.get("dropout_percent", 0.2) + n_layer: int = kwargs.get("n_layer", 1) self.input_layer = nn.Linear(input_dim, hidden_dim) - self.hidden_layer = nn.Linear(hidden_dim, hidden_dim) + self.blocks = nn.Sequential(*[Block(hidden_dim, dropout_percent) for _ in range(n_layer)]) self.output_layer = nn.Linear(hidden_dim, output_dim) self.relu = nn.ReLU() - self.dropout = nn.Dropout(p=0.2) + self.dropout = nn.Dropout(p=dropout_percent) def forward(self, x: Tensor) -> Tensor: x = self.relu(self.input_layer(x)) x = self.dropout(x) - x = self.relu(self.hidden_layer(x)) - x = self.dropout(x) + x = self.relu(self.blocks(x)) logits = self.output_layer(x) return logits + + +class Block(nn.Module): + def __init__(self, hidden_dim: int, dropout_percent: int): + super(Block, self).__init__() + self.ff = FeedForward(hidden_dim) + self.dropout = nn.Dropout(p=dropout_percent) + self.ln = nn.LayerNorm(hidden_dim) + + def forward(self, x): + x = self.dropout(self.ff(x)) + x = self.ln(x) + return x + + +class FeedForward(nn.Module): + def __init__(self, hidden_dim: int): + super(FeedForward, self).__init__() + self.net = nn.Sequential( + nn.Linear(hidden_dim, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, hidden_dim), + ) + + def forward(self, x): + return self.net(x) From cb17b36981ba8513c5d0a370864b0cfe312c0c52 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 12 Mar 2023 14:50:08 +0200 Subject: [PATCH 030/208] simplify file_type check comparisons --- freqtrade/freqai/data_drawer.py | 3 +-- freqtrade/freqai/freqai_interface.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 8d31586fe..fd839ad2f 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -447,8 +447,7 @@ class FreqaiDataDrawer: elif self.model_type == 'keras': model.save(save_path / f"{dk.model_filename}_model.h5") elif ('stable_baselines' in self.model_type or - 'sb3_contrib' == self.model_type or - 'pytorch' == self.model_type): + self.model_type in ['sb3_contrib', 'pytorch']): model.save(save_path / f"{dk.model_filename}_model.zip") if dk.svm_model is not None: diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 79bd7d672..3d06745f9 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -564,8 +564,7 @@ class IFreqaiModel(ABC): elif self.dd.model_type == 'keras': file_type = ".h5" elif ('stable_baselines' in self.dd.model_type or - 'sb3_contrib' == self.dd.model_type or - 'pytorch' == self.dd.model_type): + self.dd.model_type in ['sb3_contrib', 'pytorch']): file_type = ".zip" path_to_modelfile = Path(dk.data_path / f"{dk.model_filename}_model{file_type}") From 0012fe36ca6e6a556936039aad77d054c0cdef25 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 12 Mar 2023 16:16:04 +0200 Subject: [PATCH 031/208] sort imports --- .../freqai/prediction_models/PyTorchClassifierMultiTarget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index be42fd8e6..2855dda33 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, List, Tuple, Optional +from typing import Any, Dict, List, Optional, Tuple import numpy as np import numpy.typing as npt From 523a58d3d67ee058df60de0c059053bfe307f199 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 13 Mar 2023 00:16:44 +0200 Subject: [PATCH 032/208] simplify statement for pytorch file_type extension --- freqtrade/freqai/data_drawer.py | 3 +-- freqtrade/freqai/freqai_interface.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index fd839ad2f..aaf9a869c 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -446,8 +446,7 @@ class FreqaiDataDrawer: dump(model, save_path / f"{dk.model_filename}_model.joblib") elif self.model_type == 'keras': model.save(save_path / f"{dk.model_filename}_model.h5") - elif ('stable_baselines' in self.model_type or - self.model_type in ['sb3_contrib', 'pytorch']): + elif self.model_type in ["stable_baselines", "sb3_contrib", "pytorch"]: model.save(save_path / f"{dk.model_filename}_model.zip") if dk.svm_model is not None: diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 3d06745f9..7c45d4642 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -563,8 +563,7 @@ class IFreqaiModel(ABC): file_type = ".joblib" elif self.dd.model_type == 'keras': file_type = ".h5" - elif ('stable_baselines' in self.dd.model_type or - self.dd.model_type in ['sb3_contrib', 'pytorch']): + elif self.dd.model_type in ["stable_baselines", "sb3_contrib", "pytorch"]: file_type = ".zip" path_to_modelfile = Path(dk.data_path / f"{dk.model_filename}_model{file_type}") From b927c9dc01277368f3a944779f81ece329d24080 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 13 Mar 2023 00:17:34 +0200 Subject: [PATCH 033/208] remove train loss calculation from estimate_loss --- .../freqai/base_models/PyTorchModelTrainer.py | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 99ee44e3b..1b328f4fe 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -66,14 +66,9 @@ class PyTorchModelTrainer: n_iters=self.max_iters ) for epoch in range(epochs): - # evaluation - losses = self.estimate_loss(data_loaders_dictionary, self.max_n_eval_batches) - logger.info( - f"epoch ({epoch}/{epochs}):" - f" train loss {losses['train']:.4f} ; test loss {losses['test']:.4f}" - ) # training - for batch_data in data_loaders_dictionary["train"]: + losses = [] + for i, batch_data in enumerate(data_loaders_dictionary["train"]): xb, yb = batch_data xb = xb.to(self.device) yb = yb.to(self.device) @@ -83,35 +78,40 @@ class PyTorchModelTrainer: self.optimizer.zero_grad(set_to_none=True) loss.backward() self.optimizer.step() + losses.append(loss.item()) + train_loss = sum(losses) / len(losses) + + # evaluation + test_loss = self.estimate_loss(data_loaders_dictionary, self.max_n_eval_batches, "test") + logger.info( + f"epoch ({epoch}/{epochs}):" + f" train loss {train_loss:.4f} ; test loss {test_loss:.4f}" + ) @torch.no_grad() def estimate_loss( self, data_loader_dictionary: Dict[str, DataLoader], - max_n_eval_batches: Optional[int] - ) -> Dict[str, float]: - + max_n_eval_batches: Optional[int], + split: str, + ) -> float: self.model.eval() - loss_dictionary = {} n_batches = 0 - for split in ["train", "test"]: - losses = [] - for i, batch in enumerate(data_loader_dictionary[split]): - if max_n_eval_batches and i > max_n_eval_batches: - n_batches += 1 - break + losses = [] + for i, batch in enumerate(data_loader_dictionary[split]): + if max_n_eval_batches and i > max_n_eval_batches: + n_batches += 1 + break - xb, yb = batch - xb = xb.to(self.device) - yb = yb.to(self.device) - yb_pred = self.model(xb) - loss = self.criterion(yb_pred, yb) - losses.append(loss.item()) - - loss_dictionary[split] = sum(losses) / len(losses) + xb, yb = batch + xb = xb.to(self.device) + yb = yb.to(self.device) + yb_pred = self.model(xb) + loss = self.criterion(yb_pred, yb) + losses.append(loss.item()) self.model.train() - return loss_dictionary + return sum(losses) / len(losses) def create_data_loaders_dictionary( self, From b6096efadd575112842526dfe67d66b6fdcf9a5a Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 13 Mar 2023 00:35:14 +0200 Subject: [PATCH 034/208] logging change --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 1b328f4fe..90fb472e5 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -84,7 +84,7 @@ class PyTorchModelTrainer: # evaluation test_loss = self.estimate_loss(data_loaders_dictionary, self.max_n_eval_batches, "test") logger.info( - f"epoch ({epoch}/{epochs}):" + f"epoch {epoch}/{epochs}:" f" train loss {train_loss:.4f} ; test loss {test_loss:.4f}" ) From d7ea75082384c160217a7382798b42e48c6e70fb Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 13 Mar 2023 00:35:51 +0200 Subject: [PATCH 035/208] revert to using model_training_parameters --- .../PyTorchClassifierMultiTarget.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index 2855dda33..3abc56fb1 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -34,13 +34,15 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): """ super().__init__(**kwargs) - trainer_kwargs = self.freqai_info.get("trainer_kwargs", {}) - self.n_hidden: int = trainer_kwargs.get("n_hidden", 1024) - self.max_iters: int = trainer_kwargs.get("max_iters", 100) - self.batch_size: int = trainer_kwargs.get("batch_size", 64) - self.learning_rate: float = trainer_kwargs.get("learning_rate", 3e-4) - self.max_n_eval_batches: Optional[int] = trainer_kwargs.get("max_n_eval_batches", None) - self.model_kwargs: Dict = trainer_kwargs.get("model_kwargs", {}) + model_training_params = self.freqai_info.get("model_training_parameters", {}) + self.n_hidden: int = model_training_params.get("n_hidden", 1024) + self.max_iters: int = model_training_params.get("max_iters", 100) + self.batch_size: int = model_training_params.get("batch_size", 64) + self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) + self.max_n_eval_batches: Optional[int] = model_training_params.get( + "max_n_eval_batches", None + ) + self.model_kwargs: Dict = model_training_params.get("model_kwargs", {}) self.class_name_to_index = None self.index_to_class_name = None From 9c8c30b0e8b804f2b9b67de8d752c532313e59d7 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 13 Mar 2023 17:17:00 +0200 Subject: [PATCH 036/208] add test --- tests/freqai/test_freqai_interface.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index f8bee3659..da3c28de8 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -52,7 +52,8 @@ def can_run_model(model: str) -> None: ('ReinforcementLearner_multiproc', False, False, False, True, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, False, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, True, False, 0), - ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0) + ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0), + ('PyTorchClassifierMultiTarget', False, False, False, True, False, 0) ]) def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, dbscan, float32, can_short, shuffle, buffer): @@ -85,6 +86,21 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'test_3ac' in model or 'test_4ac' in model: freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") + if 'PyTorchClassifierMultiTarget' in model: + model_save_ext = 'zip' + freqai_conf['freqai']['model_training_parameters'].update({ + "n_hidden": 1024, + "max_iters": 100, + "batch_size": 64, + "learning_rate": 3e-4, + "max_n_eval_batches": None, + "model_kwargs": { + "hidden_dim": 1024, + "dropout_percent": 0.2, + "n_layer": 1, + } + }) + strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) From 918889a2bdbe53d4c502da8d4ce89f8380bda357 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 13 Mar 2023 20:09:12 +0200 Subject: [PATCH 037/208] reduce mlp number of parameters for testing --- .../freqai/prediction_models/PyTorchClassifierMultiTarget.py | 1 - tests/freqai/test_freqai_interface.py | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index 3abc56fb1..edafb3b7a 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -35,7 +35,6 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): super().__init__(**kwargs) model_training_params = self.freqai_info.get("model_training_parameters", {}) - self.n_hidden: int = model_training_params.get("n_hidden", 1024) self.max_iters: int = model_training_params.get("max_iters", 100) self.batch_size: int = model_training_params.get("batch_size", 64) self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index da3c28de8..3b31012b2 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -89,13 +89,12 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'PyTorchClassifierMultiTarget' in model: model_save_ext = 'zip' freqai_conf['freqai']['model_training_parameters'].update({ - "n_hidden": 1024, - "max_iters": 100, + "max_iters": 1, "batch_size": 64, "learning_rate": 3e-4, "max_n_eval_batches": None, "model_kwargs": { - "hidden_dim": 1024, + "hidden_dim": 32, "dropout_percent": 0.2, "n_layer": 1, } From 366740885a85aa9111aed8cfa8a92d9c5b862e07 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 13 Mar 2023 20:18:26 +0200 Subject: [PATCH 038/208] reduce mlp number of parameters for testing --- tests/freqai/test_freqai_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 3b31012b2..f34659398 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -92,7 +92,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, "max_iters": 1, "batch_size": 64, "learning_rate": 3e-4, - "max_n_eval_batches": None, + "max_n_eval_batches": 1, "model_kwargs": { "hidden_dim": 32, "dropout_percent": 0.2, From 4550447409a7895de8627718d03bd6bfd14577df Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 14 Mar 2023 21:13:30 +0100 Subject: [PATCH 039/208] cheat flake8 for now until we can refactor save into the model class --- freqtrade/freqai/data_drawer.py | 4 ++-- freqtrade/freqai/freqai_interface.py | 2 +- .../freqai/prediction_models/PyTorchClassifierMultiTarget.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index aaf9a869c..c8dadb171 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -446,7 +446,7 @@ class FreqaiDataDrawer: dump(model, save_path / f"{dk.model_filename}_model.joblib") elif self.model_type == 'keras': model.save(save_path / f"{dk.model_filename}_model.h5") - elif self.model_type in ["stable_baselines", "sb3_contrib", "pytorch"]: + elif self.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]: model.save(save_path / f"{dk.model_filename}_model.zip") if dk.svm_model is not None: @@ -496,7 +496,7 @@ class FreqaiDataDrawer: dk.training_features_list = dk.data["training_features_list"] dk.label_list = dk.data["label_list"] - def load_data(self, coin: str, dk: FreqaiDataKitchen) -> Any: + def load_data(self, coin: str, dk: FreqaiDataKitchen) -> Any: # noqa: C901 """ loads all data required to make a prediction on a sub-train time range :returns: diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 7c45d4642..8a1ac436b 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -563,7 +563,7 @@ class IFreqaiModel(ABC): file_type = ".joblib" elif self.dd.model_type == 'keras': file_type = ".h5" - elif self.dd.model_type in ["stable_baselines", "sb3_contrib", "pytorch"]: + elif self.dd.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]: file_type = ".zip" path_to_modelfile = Path(dk.data_path / f"{dk.model_filename}_model{file_type}") diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index edafb3b7a..967199c12 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -41,7 +41,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): self.max_n_eval_batches: Optional[int] = model_training_params.get( "max_n_eval_batches", None ) - self.model_kwargs: Dict = model_training_params.get("model_kwargs", {}) + self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) self.class_name_to_index = None self.index_to_class_name = None From 244662b1a45a3d5c394936400d805215bf7508e3 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sat, 18 Mar 2023 14:12:31 +0200 Subject: [PATCH 040/208] set class names attribute in the general classifier testing strategy --- tests/strategy/strats/freqai_test_classifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/strategy/strats/freqai_test_classifier.py b/tests/strategy/strats/freqai_test_classifier.py index 61b9f0c37..a68a87b2a 100644 --- a/tests/strategy/strats/freqai_test_classifier.py +++ b/tests/strategy/strats/freqai_test_classifier.py @@ -82,7 +82,7 @@ class freqai_test_classifier(IStrategy): return dataframe def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): - + self.freqai.class_names = ["down", "up"] dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) > dataframe["close"], 'up', 'down') From fab9ff129461141dfaa4a3b69d4dccb4e922e955 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sat, 18 Mar 2023 15:27:38 +0200 Subject: [PATCH 041/208] fix tests --- tests/freqai/test_freqai_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index f34659398..c35d1afb4 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -52,8 +52,7 @@ def can_run_model(model: str) -> None: ('ReinforcementLearner_multiproc', False, False, False, True, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, False, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, True, False, 0), - ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0), - ('PyTorchClassifierMultiTarget', False, False, False, True, False, 0) + ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0) ]) def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, dbscan, float32, can_short, shuffle, buffer): @@ -183,6 +182,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'CatboostClassifier', 'XGBoostClassifier', 'XGBoostRFClassifier', + 'PyTorchClassifierMultiTarget', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): if (is_arm() or is_py11()) and model == 'CatboostClassifier': From a49f62eecbb9838e211300964c61ba889214cb80 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sat, 18 Mar 2023 20:51:30 +0200 Subject: [PATCH 042/208] classifier test - set model file extension --- tests/freqai/test_freqai_interface.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index c35d1afb4..a5fe9b90b 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -210,8 +210,16 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + if freqai.dd.model_type == 'joblib': + model_file_extension = ".joblib" + elif freqai.dd.model_type == "pytorch": + model_file_extension = ".zip" + else: + raise Exception(f"Unsupported model type: {freqai.dd.model_type}," + f" can't assign model_file_extension") - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").exists() + assert Path(freqai.dk.data_path / + f"{freqai.dk.model_filename}_model{model_file_extension}").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").exists() From 366c148c10769e745e7748f02503f0ccecca994f Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 14:38:49 +0200 Subject: [PATCH 043/208] create children class to PyTorchClassifier to implement the fit method where we initialize the trainer and model objects --- .../freqai/base_models/PyTorchModelTrainer.py | 19 ++- .../prediction_models/MLPPyTorchClassifier.py | 81 ++++++++++++ ...rget.py => PyTorchClassifierClassifier.py} | 125 ++++++++---------- tests/freqai/test_freqai_interface.py | 8 +- 4 files changed, 146 insertions(+), 87 deletions(-) create mode 100644 freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py rename freqtrade/freqai/prediction_models/{PyTorchClassifierMultiTarget.py => PyTorchClassifierClassifier.py} (53%) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 90fb472e5..f91b44924 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -19,35 +19,32 @@ class PyTorchModelTrainer: optimizer: Optimizer, criterion: nn.Module, device: str, - batch_size: int, - max_iters: int, - max_n_eval_batches: int, init_model: Dict, model_meta_data: Dict[str, Any] = {}, + **kwargs ): """ :param model: The PyTorch model to be trained. :param optimizer: The optimizer to use for training. :param criterion: The loss function to use for training. :param device: The device to use for training (e.g. 'cpu', 'cuda'). - :param batch_size: The size of the batches to use during training. + :param init_model: A dictionary containing the initial model/optimizer + state_dict and model_meta_data saved by self.save() method. + :param model_meta_data: Additional metadata about the model (optional). :param max_iters: The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs. + :param batch_size: The size of the batches to use during training. :param max_n_eval_batches: The maximum number batches to use for evaluation. - :param init_model: A dictionary containing the initial model/optimizer - state_dict and model_meta_data saved by self.save() method. - :param model_meta_data: Additional metadata about the model (optional). """ self.model = model self.optimizer = optimizer self.criterion = criterion self.model_meta_data = model_meta_data self.device = device - self.max_iters = max_iters - self.batch_size = batch_size - self.max_n_eval_batches = max_n_eval_batches - + self.max_iters: int = kwargs.get("max_iters", 100) + self.batch_size: int = kwargs.get("batch_size", 64) + self.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None) if init_model: self.load_from_checkpoint(init_model) diff --git a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py b/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py new file mode 100644 index 000000000..d6be8c1df --- /dev/null +++ b/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py @@ -0,0 +1,81 @@ +from typing import Any, Dict + +from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.PyTorchClassifierClassifier import PyTorchClassifier +from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel + +import torch + + +class MLPPyTorchClassifier(PyTorchClassifier): + """ + This class implements the fit method of IFreqaiModel. + int the fit method we initialize the model and trainer objects. + the only requirement from the model is to be aligned to PyTorchClassifier + predict method that expects the model to predict tensor of type long. + the trainer defines the training loop. + + parameters are passed via `model_training_parameters` under the freqai + section in the config file. e.g: + { + ... + "freqai": { + ... + "model_training_parameters" : { + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 5000, + "batch_size": 64, + "max_n_eval_batches": None, + }, + "model_kwargs": { + "hidden_dim": 512, + "dropout_percent": 0.2, + "n_layer": 1, + }, + } + } + } + + + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + model_training_params = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, any] = model_training_params.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + :raises ValueError: If self.class_names is not defined in the parent class. + """ + + class_names = self.get_class_names() + self.convert_label_column_to_int(data_dictionary, dk, class_names) + n_features = data_dictionary["train_features"].shape[-1] + model = PyTorchMLPModel( + input_dim=n_features, + output_dim=len(class_names), + **self.model_kwargs + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.CrossEntropyLoss() + init_model = self.get_init_model(dk.pair) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + model_meta_data={"class_names": class_names}, + device=self.device, + init_model=init_model, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary) + return trainer diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py similarity index 53% rename from freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py rename to freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py index 967199c12..0be10b31e 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, List, Optional, Tuple +from typing import Dict, List, Tuple import numpy as np import numpy.typing as npt @@ -10,17 +10,16 @@ from torch.nn import functional as F from freqtrade.exceptions import OperationalException from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel -from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel logger = logging.getLogger(__name__) -class PyTorchClassifierMultiTarget(BasePyTorchModel): +class PyTorchClassifier(BasePyTorchModel): """ - A PyTorch implementation of a multi-target classifier. + A PyTorch implementation of a classifier. + User must implement fit method """ def __init__(self, **kwargs): """ @@ -34,59 +33,9 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): """ super().__init__(**kwargs) - model_training_params = self.freqai_info.get("model_training_parameters", {}) - self.max_iters: int = model_training_params.get("max_iters", 100) - self.batch_size: int = model_training_params.get("batch_size", 64) - self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) - self.max_n_eval_batches: Optional[int] = model_training_params.get( - "max_n_eval_batches", None - ) - self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) self.class_name_to_index = None self.index_to_class_name = None - def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: - """ - User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. - :raises ValueError: If self.class_names is not defined in the parent class. - - """ - - if not hasattr(self, "class_names"): - raise ValueError( - "Missing attribute: self.class_names " - "set self.freqai.class_names = [\"class a\", \"class b\", \"class c\"] " - "inside IStrategy.set_freqai_targets method." - ) - - self.init_class_names_to_index_mapping(self.class_names) - self.encode_classes_name(data_dictionary, dk) - n_features = data_dictionary["train_features"].shape[-1] - model = PyTorchMLPModel( - input_dim=n_features, - output_dim=len(self.class_names), - **self.model_kwargs - ) - model.to(self.device) - optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) - criterion = torch.nn.CrossEntropyLoss() - init_model = self.get_init_model(dk.pair) - trainer = PyTorchModelTrainer( - model=model, - optimizer=optimizer, - criterion=criterion, - model_meta_data={"class_names": self.class_names}, - device=self.device, - batch_size=self.batch_size, - max_iters=self.max_iters, - max_n_eval_batches=self.max_n_eval_batches, - init_model=init_model - ) - trainer.fit(data_dictionary) - return trainer - def predict( self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: @@ -97,7 +46,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): :pred_df: dataframe containing the predictions :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove data (NaNs) or felt uncertain about data (PCA and DI index) - :raises ValueError: if 'class_name' doesn't exist in model meta_data. + :raises ValueError: if 'class_names' doesn't exist in model meta_data. """ class_names = self.model.model_meta_data.get("class_names", None) @@ -106,7 +55,9 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): "Missing class names. " "self.model.model_meta_data[\"class_names\"] is None." ) - self.init_class_names_to_index_mapping(class_names) + + if not self.class_name_to_index: + self.init_class_names_to_index_mapping(class_names) dk.find_features(unfiltered_df) filtered_df, _ = dk.filter_features( @@ -116,49 +67,77 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): dk.data_dictionary["prediction_features"] = filtered_df self.data_cleaning_predict(dk) - dk.data_dictionary["prediction_features"] = torch.tensor( - dk.data_dictionary["prediction_features"].values - ).float().to(self.device) + x = torch.from_numpy(dk.data_dictionary["prediction_features"].values)\ + .float()\ + .to(self.device) - logits = self.model.model(dk.data_dictionary["prediction_features"]) + logits = self.model.model(x) probs = F.softmax(logits, dim=-1) predicted_classes = torch.argmax(probs, dim=-1) - predicted_classes_str = self.decode_classes_name(predicted_classes) + predicted_classes_str = self.decode_class_names(predicted_classes) pred_df_prob = DataFrame(probs.detach().numpy(), columns=class_names) pred_df = DataFrame(predicted_classes_str, columns=[dk.label_list[0]]) pred_df = pd.concat([pred_df, pred_df_prob], axis=1) return (pred_df, dk.do_predict) - def encode_classes_name(self, data_dictionary: Dict[str, pd.DataFrame], dk: FreqaiDataKitchen): + def encode_class_names( + self, + data_dictionary: Dict[str, pd.DataFrame], + dk: FreqaiDataKitchen, + class_names: List[str], + ): """ - encode class name str -> int - assuming first column of *_labels data frame to contain class names + encode class name, str -> int + assuming first column of *_labels data frame to be the target column + containing the class names """ target_column_name = dk.label_list[0] for split in ["train", "test"]: label_df = data_dictionary[f"{split}_labels"] - self.assert_valid_class_names(label_df[target_column_name]) + self.assert_valid_class_names(label_df[target_column_name], class_names) label_df[target_column_name] = list( map(lambda x: self.class_name_to_index[x], label_df[target_column_name]) ) - def assert_valid_class_names(self, labels: pd.Series): - non_defined_labels = set(labels) - set(self.class_names) + @staticmethod + def assert_valid_class_names( + target_column: pd.Series, + class_names: List[str] + ): + non_defined_labels = set(target_column) - set(class_names) if len(non_defined_labels) != 0: raise OperationalException( f"Found non defined labels: {non_defined_labels}, ", - f"expecting labels: {self.class_names}" + f"expecting labels: {class_names}" ) - def decode_classes_name(self, classes: torch.Tensor) -> List[str]: + def decode_class_names(self, class_ints: torch.Tensor) -> List[str]: """ - decode class name int -> str + decode class name, int -> str """ - return list(map(lambda x: self.index_to_class_name[x.item()], classes)) + return list(map(lambda x: self.index_to_class_name[x.item()], class_ints)) def init_class_names_to_index_mapping(self, class_names): self.class_name_to_index = {s: i for i, s in enumerate(class_names)} self.index_to_class_name = {i: s for i, s in enumerate(class_names)} - logger.info(f"class_name_to_index: {self.class_name_to_index}") + logger.info(f"encoded class name to index: {self.class_name_to_index}") + + def convert_label_column_to_int( + self, + data_dictionary: Dict[str, pd.DataFrame], + dk: FreqaiDataKitchen, + class_names: List[str] + ): + self.init_class_names_to_index_mapping(class_names) + self.encode_class_names(data_dictionary, dk, class_names) + + def get_class_names(self) -> List[str]: + if not hasattr(self, "class_names"): + raise ValueError( + "Missing attribute: self.class_names " + "set self.freqai.class_names = [\"class a\", \"class b\", \"class c\"] " + "inside IStrategy.set_freqai_targets method." + ) + return self.class_names diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index a5fe9b90b..181c0539d 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -88,10 +88,12 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'PyTorchClassifierMultiTarget' in model: model_save_ext = 'zip' freqai_conf['freqai']['model_training_parameters'].update({ - "max_iters": 1, - "batch_size": 64, "learning_rate": 3e-4, - "max_n_eval_batches": 1, + "trainer_kwargs": { + "max_iters": 1, + "batch_size": 64, + "max_n_eval_batches": 1, + }, "model_kwargs": { "hidden_dim": 32, "dropout_percent": 0.2, From 61ac36c576e2dfafeba560ce5fe96a1fc5735d34 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 14:49:12 +0200 Subject: [PATCH 044/208] fix test --- tests/freqai/test_freqai_interface.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 181c0539d..33c18677b 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -85,22 +85,6 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'test_3ac' in model or 'test_4ac' in model: freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") - if 'PyTorchClassifierMultiTarget' in model: - model_save_ext = 'zip' - freqai_conf['freqai']['model_training_parameters'].update({ - "learning_rate": 3e-4, - "trainer_kwargs": { - "max_iters": 1, - "batch_size": 64, - "max_n_eval_batches": 1, - }, - "model_kwargs": { - "hidden_dim": 32, - "dropout_percent": 0.2, - "n_layer": 1, - } - }) - strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) @@ -184,7 +168,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'CatboostClassifier', 'XGBoostClassifier', 'XGBoostRFClassifier', - 'PyTorchClassifierMultiTarget', + 'PyTorchClassifier', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): if (is_arm() or is_py11()) and model == 'CatboostClassifier': From 9f477aa3c9c7de59d44db45088ef30e1027b93e0 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 15:09:50 +0200 Subject: [PATCH 045/208] sort imports --- freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py b/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py index d6be8c1df..2f6705311 100644 --- a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py +++ b/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py @@ -1,12 +1,12 @@ from typing import Any, Dict +import torch + from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.PyTorchClassifierClassifier import PyTorchClassifier from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel -import torch - class MLPPyTorchClassifier(PyTorchClassifier): """ From 719faab4b8301410f86956cf09d698529a2ee93b Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 15:21:34 +0200 Subject: [PATCH 046/208] fix test --- tests/freqai/test_freqai_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 33c18677b..d183501ea 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -168,7 +168,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'CatboostClassifier', 'XGBoostClassifier', 'XGBoostRFClassifier', - 'PyTorchClassifier', + 'MLPPyTorchClassifier', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): if (is_arm() or is_py11()) and model == 'CatboostClassifier': From 8bee499328e9d242361fb11289b63bca6bf17d9d Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 17:03:36 +0200 Subject: [PATCH 047/208] modify feedforward net, move layer norm to start of thr block --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 91e496c5d..482b3f889 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -22,7 +22,7 @@ class PyTorchMLPModel(nn.Module): def forward(self, x: Tensor) -> Tensor: x = self.relu(self.input_layer(x)) x = self.dropout(x) - x = self.relu(self.blocks(x)) + x = self.blocks(x) logits = self.output_layer(x) return logits @@ -35,8 +35,8 @@ class Block(nn.Module): self.ln = nn.LayerNorm(hidden_dim) def forward(self, x): - x = self.dropout(self.ff(x)) - x = self.ln(x) + x = self.ff(self.ln(x)) + x = self.dropout(x) return x @@ -46,7 +46,6 @@ class FeedForward(nn.Module): self.net = nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), - nn.Linear(hidden_dim, hidden_dim), ) def forward(self, x): From 6f9a8a089cd5bd54ca91d0d5c57231d75519af2d Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 17:45:30 +0200 Subject: [PATCH 048/208] add mlp documentation --- .../prediction_models/PyTorchMLPModel.py | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 482b3f889..07056e930 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -1,16 +1,42 @@ import logging import torch.nn as nn -from torch import Tensor - +import torch logger = logging.getLogger(__name__) class PyTorchMLPModel(nn.Module): + """ + A multi-layer perceptron (MLP) model implemented using PyTorch. + + :param input_dim: The number of input features. + :param output_dim: The number of output classes. + :param hidden_dim: The number of hidden units in each layer. Default: 256 + :param dropout_percent: The dropout rate for regularization. Default: 0.2 + :param n_layer: The number of layers in the MLP. Default: 1 + + :returns: The output of the MLP, with shape (batch_size, output_dim) + + + A neural network typically consists of input, output, and hidden layers, where the + information flows from the input layer through the hidden layers to the output layer. + In a feedforward neural network, also known as a multilayer perceptron (MLP), the + information flows in one direction only. Each hidden layer contains multiple units + or nodes that take input from the previous layer and produce output that goes to the + next layer. + + The hidden_dim parameter in the FeedForward class refers to the number of units + (or nodes) in the hidden layer. This parameter controls the complexity of the neural + network and determines how many nonlinear relationships the network can represent. + A higher value of hidden_dim allows the network to represent more complex functions + but may also make the network more prone to overfitting, where the model memorizes + the training data instead of learning general patterns. + """ + def __init__(self, input_dim: int, output_dim: int, **kwargs): super(PyTorchMLPModel, self).__init__() - hidden_dim: int = kwargs.get("hidden_dim", 1024) + hidden_dim: int = kwargs.get("hidden_dim", 256) dropout_percent: int = kwargs.get("dropout_percent", 0.2) n_layer: int = kwargs.get("n_layer", 1) self.input_layer = nn.Linear(input_dim, hidden_dim) @@ -19,7 +45,7 @@ class PyTorchMLPModel(nn.Module): self.relu = nn.ReLU() self.dropout = nn.Dropout(p=dropout_percent) - def forward(self, x: Tensor) -> Tensor: + def forward(self, x: torch.Tensor) -> torch.Tensor: x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.blocks(x) @@ -28,19 +54,35 @@ class PyTorchMLPModel(nn.Module): class Block(nn.Module): + """ + A building block for a multi-layer perceptron (MLP) implemented using PyTorch. + + :param hidden_dim: The number of hidden units in the feedforward network. + :param dropout_percent: The dropout rate for regularization. + + :returns: torch.Tensor. with shape (batch_size, hidden_dim) + """ + def __init__(self, hidden_dim: int, dropout_percent: int): super(Block, self).__init__() self.ff = FeedForward(hidden_dim) self.dropout = nn.Dropout(p=dropout_percent) self.ln = nn.LayerNorm(hidden_dim) - def forward(self, x): + def forward(self, x: torch.Tensor) -> torch.Tensor: x = self.ff(self.ln(x)) x = self.dropout(x) return x class FeedForward(nn.Module): + """ + A fully-connected feedforward neural network block. + + :param hidden_dim: The number of hidden units in the block. + :return: torch.Tensor. with shape (batch_size, hidden_dim) + """ + def __init__(self, hidden_dim: int): super(FeedForward, self).__init__() self.net = nn.Sequential( @@ -48,5 +90,5 @@ class FeedForward(nn.Module): nn.ReLU(), ) - def forward(self, x): + def forward(self, x: torch.Tensor) -> torch.Tensor: return self.net(x) From 903a1dc3e52507457e46242348beb2e6ea54ad45 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 18:04:01 +0200 Subject: [PATCH 049/208] improve mlp documentation --- .../prediction_models/PyTorchMLPModel.py | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 07056e930..c2b6c1b93 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -10,28 +10,25 @@ class PyTorchMLPModel(nn.Module): """ A multi-layer perceptron (MLP) model implemented using PyTorch. - :param input_dim: The number of input features. - :param output_dim: The number of output classes. - :param hidden_dim: The number of hidden units in each layer. Default: 256 - :param dropout_percent: The dropout rate for regularization. Default: 0.2 - :param n_layer: The number of layers in the MLP. Default: 1 + :param input_dim: The number of input features. This parameter specifies the number + of features in the input data that the MLP will use to make predictions. + :param output_dim: The number of output classes. This parameter specifies the number + of classes that the MLP will predict. + :param hidden_dim: The number of hidden units in each layer. This parameter controls + the complexity of the MLP and determines how many nonlinear relationships the MLP + can represent. Increasing the number of hidden units can increase the capacity of + the MLP to model complex patterns, but it also increases the risk of overfitting + the training data. Default: 256 + :param dropout_percent: The dropout rate for regularization. This parameter specifies + the probability of dropping out a neuron during training to prevent overfitting. + The dropout rate should be tuned carefully to balance between underfitting and + overfitting. Default: 0.2 + :param n_layer: The number of layers in the MLP. This parameter specifies the number + of layers in the MLP architecture. Adding more layers to the MLP can increase its + capacity to model complex patterns, but it also increases the risk of overfitting + the training data. Default: 1 :returns: The output of the MLP, with shape (batch_size, output_dim) - - - A neural network typically consists of input, output, and hidden layers, where the - information flows from the input layer through the hidden layers to the output layer. - In a feedforward neural network, also known as a multilayer perceptron (MLP), the - information flows in one direction only. Each hidden layer contains multiple units - or nodes that take input from the previous layer and produce output that goes to the - next layer. - - The hidden_dim parameter in the FeedForward class refers to the number of units - (or nodes) in the hidden layer. This parameter controls the complexity of the neural - network and determines how many nonlinear relationships the network can represent. - A higher value of hidden_dim allows the network to represent more complex functions - but may also make the network more prone to overfitting, where the model memorizes - the training data instead of learning general patterns. """ def __init__(self, input_dim: int, output_dim: int, **kwargs): @@ -55,7 +52,7 @@ class PyTorchMLPModel(nn.Module): class Block(nn.Module): """ - A building block for a multi-layer perceptron (MLP) implemented using PyTorch. + A building block for a multi-layer perceptron (MLP). :param hidden_dim: The number of hidden units in the feedforward network. :param dropout_percent: The dropout rate for regularization. @@ -77,7 +74,7 @@ class Block(nn.Module): class FeedForward(nn.Module): """ - A fully-connected feedforward neural network block. + A simple fully-connected feedforward neural network block. :param hidden_dim: The number of hidden units in the block. :return: torch.Tensor. with shape (batch_size, hidden_dim) From 1c11a5f0485d2bdca5295712f4607abc0fe6d6a9 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 18:10:57 +0200 Subject: [PATCH 050/208] improve mlp documentation --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index c2b6c1b93..0e6b3c7bb 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -10,6 +10,9 @@ class PyTorchMLPModel(nn.Module): """ A multi-layer perceptron (MLP) model implemented using PyTorch. + This class mainly serves as a simple example for the integration of PyTorch model's + to freqai. It is not optimized at all and should not be used for production purposes. + :param input_dim: The number of input features. This parameter specifies the number of features in the input data that the MLP will use to make predictions. :param output_dim: The number of output classes. This parameter specifies the number From 2f386913ac0406371790b38d92f3b76c234552a8 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 11:54:17 +0200 Subject: [PATCH 051/208] refactor classifiers class names --- .../{PyTorchClassifierClassifier.py => PyTorchClassifier.py} | 0 .../{MLPPyTorchClassifier.py => PyTorchMLPClassifier.py} | 4 ++-- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename freqtrade/freqai/prediction_models/{PyTorchClassifierClassifier.py => PyTorchClassifier.py} (100%) rename freqtrade/freqai/prediction_models/{MLPPyTorchClassifier.py => PyTorchMLPClassifier.py} (95%) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py b/freqtrade/freqai/prediction_models/PyTorchClassifier.py similarity index 100% rename from freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py rename to freqtrade/freqai/prediction_models/PyTorchClassifier.py diff --git a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py similarity index 95% rename from freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py rename to freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 2f6705311..453995ce8 100644 --- a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -4,11 +4,11 @@ import torch from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.PyTorchClassifierClassifier import PyTorchClassifier +from freqtrade.freqai.prediction_models.PyTorchClassifier import PyTorchClassifier from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel -class MLPPyTorchClassifier(PyTorchClassifier): +class PyTorchMLPClassifier(PyTorchClassifier): """ This class implements the fit method of IFreqaiModel. int the fit method we initialize the model and trainer objects. diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 0e6b3c7bb..f711a53a7 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -49,8 +49,8 @@ class PyTorchMLPModel(nn.Module): x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.blocks(x) - logits = self.output_layer(x) - return logits + x = self.output_layer(x) + return x class Block(nn.Module): From fab505be1bde88aa772c5ff482a8e9ec6c4e90c0 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 14 Mar 2023 21:13:30 +0100 Subject: [PATCH 052/208] cheat flake8 for now until we can refactor save into the model class --- freqtrade/freqai/data_drawer.py | 4 ++-- freqtrade/freqai/freqai_interface.py | 2 +- .../freqai/prediction_models/PyTorchClassifierMultiTarget.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index aaf9a869c..c8dadb171 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -446,7 +446,7 @@ class FreqaiDataDrawer: dump(model, save_path / f"{dk.model_filename}_model.joblib") elif self.model_type == 'keras': model.save(save_path / f"{dk.model_filename}_model.h5") - elif self.model_type in ["stable_baselines", "sb3_contrib", "pytorch"]: + elif self.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]: model.save(save_path / f"{dk.model_filename}_model.zip") if dk.svm_model is not None: @@ -496,7 +496,7 @@ class FreqaiDataDrawer: dk.training_features_list = dk.data["training_features_list"] dk.label_list = dk.data["label_list"] - def load_data(self, coin: str, dk: FreqaiDataKitchen) -> Any: + def load_data(self, coin: str, dk: FreqaiDataKitchen) -> Any: # noqa: C901 """ loads all data required to make a prediction on a sub-train time range :returns: diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 7c45d4642..8a1ac436b 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -563,7 +563,7 @@ class IFreqaiModel(ABC): file_type = ".joblib" elif self.dd.model_type == 'keras': file_type = ".h5" - elif self.dd.model_type in ["stable_baselines", "sb3_contrib", "pytorch"]: + elif self.dd.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]: file_type = ".zip" path_to_modelfile = Path(dk.data_path / f"{dk.model_filename}_model{file_type}") diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index edafb3b7a..967199c12 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -41,7 +41,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): self.max_n_eval_batches: Optional[int] = model_training_params.get( "max_n_eval_batches", None ) - self.model_kwargs: Dict = model_training_params.get("model_kwargs", {}) + self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) self.class_name_to_index = None self.index_to_class_name = None From d0a33d2ee7aa05ac08900cf5e44be79e195633f6 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sat, 18 Mar 2023 15:27:38 +0200 Subject: [PATCH 053/208] fix tests --- tests/freqai/test_freqai_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index f34659398..c35d1afb4 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -52,8 +52,7 @@ def can_run_model(model: str) -> None: ('ReinforcementLearner_multiproc', False, False, False, True, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, False, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, True, False, 0), - ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0), - ('PyTorchClassifierMultiTarget', False, False, False, True, False, 0) + ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0) ]) def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, dbscan, float32, can_short, shuffle, buffer): @@ -183,6 +182,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'CatboostClassifier', 'XGBoostClassifier', 'XGBoostRFClassifier', + 'PyTorchClassifierMultiTarget', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): if (is_arm() or is_py11()) and model == 'CatboostClassifier': From 566346dd87cbefba1ec1f5210828661314c35164 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sat, 18 Mar 2023 20:51:30 +0200 Subject: [PATCH 054/208] classifier test - set model file extension --- tests/freqai/test_freqai_interface.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index c35d1afb4..a5fe9b90b 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -210,8 +210,16 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + if freqai.dd.model_type == 'joblib': + model_file_extension = ".joblib" + elif freqai.dd.model_type == "pytorch": + model_file_extension = ".zip" + else: + raise Exception(f"Unsupported model type: {freqai.dd.model_type}," + f" can't assign model_file_extension") - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").exists() + assert Path(freqai.dk.data_path / + f"{freqai.dk.model_filename}_model{model_file_extension}").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").exists() From 833aaf8e101027e9af5314007035ba5b6cd858fe Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 14:38:49 +0200 Subject: [PATCH 055/208] create children class to PyTorchClassifier to implement the fit method where we initialize the trainer and model objects --- .../freqai/base_models/PyTorchModelTrainer.py | 19 ++- .../prediction_models/MLPPyTorchClassifier.py | 81 ++++++++++++ ...rget.py => PyTorchClassifierClassifier.py} | 125 ++++++++---------- tests/freqai/test_freqai_interface.py | 8 +- 4 files changed, 146 insertions(+), 87 deletions(-) create mode 100644 freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py rename freqtrade/freqai/prediction_models/{PyTorchClassifierMultiTarget.py => PyTorchClassifierClassifier.py} (53%) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 90fb472e5..f91b44924 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -19,35 +19,32 @@ class PyTorchModelTrainer: optimizer: Optimizer, criterion: nn.Module, device: str, - batch_size: int, - max_iters: int, - max_n_eval_batches: int, init_model: Dict, model_meta_data: Dict[str, Any] = {}, + **kwargs ): """ :param model: The PyTorch model to be trained. :param optimizer: The optimizer to use for training. :param criterion: The loss function to use for training. :param device: The device to use for training (e.g. 'cpu', 'cuda'). - :param batch_size: The size of the batches to use during training. + :param init_model: A dictionary containing the initial model/optimizer + state_dict and model_meta_data saved by self.save() method. + :param model_meta_data: Additional metadata about the model (optional). :param max_iters: The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs. + :param batch_size: The size of the batches to use during training. :param max_n_eval_batches: The maximum number batches to use for evaluation. - :param init_model: A dictionary containing the initial model/optimizer - state_dict and model_meta_data saved by self.save() method. - :param model_meta_data: Additional metadata about the model (optional). """ self.model = model self.optimizer = optimizer self.criterion = criterion self.model_meta_data = model_meta_data self.device = device - self.max_iters = max_iters - self.batch_size = batch_size - self.max_n_eval_batches = max_n_eval_batches - + self.max_iters: int = kwargs.get("max_iters", 100) + self.batch_size: int = kwargs.get("batch_size", 64) + self.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None) if init_model: self.load_from_checkpoint(init_model) diff --git a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py b/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py new file mode 100644 index 000000000..d6be8c1df --- /dev/null +++ b/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py @@ -0,0 +1,81 @@ +from typing import Any, Dict + +from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.PyTorchClassifierClassifier import PyTorchClassifier +from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel + +import torch + + +class MLPPyTorchClassifier(PyTorchClassifier): + """ + This class implements the fit method of IFreqaiModel. + int the fit method we initialize the model and trainer objects. + the only requirement from the model is to be aligned to PyTorchClassifier + predict method that expects the model to predict tensor of type long. + the trainer defines the training loop. + + parameters are passed via `model_training_parameters` under the freqai + section in the config file. e.g: + { + ... + "freqai": { + ... + "model_training_parameters" : { + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 5000, + "batch_size": 64, + "max_n_eval_batches": None, + }, + "model_kwargs": { + "hidden_dim": 512, + "dropout_percent": 0.2, + "n_layer": 1, + }, + } + } + } + + + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + model_training_params = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, any] = model_training_params.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + :raises ValueError: If self.class_names is not defined in the parent class. + """ + + class_names = self.get_class_names() + self.convert_label_column_to_int(data_dictionary, dk, class_names) + n_features = data_dictionary["train_features"].shape[-1] + model = PyTorchMLPModel( + input_dim=n_features, + output_dim=len(class_names), + **self.model_kwargs + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.CrossEntropyLoss() + init_model = self.get_init_model(dk.pair) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + model_meta_data={"class_names": class_names}, + device=self.device, + init_model=init_model, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary) + return trainer diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py similarity index 53% rename from freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py rename to freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py index 967199c12..0be10b31e 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, List, Optional, Tuple +from typing import Dict, List, Tuple import numpy as np import numpy.typing as npt @@ -10,17 +10,16 @@ from torch.nn import functional as F from freqtrade.exceptions import OperationalException from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel -from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel logger = logging.getLogger(__name__) -class PyTorchClassifierMultiTarget(BasePyTorchModel): +class PyTorchClassifier(BasePyTorchModel): """ - A PyTorch implementation of a multi-target classifier. + A PyTorch implementation of a classifier. + User must implement fit method """ def __init__(self, **kwargs): """ @@ -34,59 +33,9 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): """ super().__init__(**kwargs) - model_training_params = self.freqai_info.get("model_training_parameters", {}) - self.max_iters: int = model_training_params.get("max_iters", 100) - self.batch_size: int = model_training_params.get("batch_size", 64) - self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) - self.max_n_eval_batches: Optional[int] = model_training_params.get( - "max_n_eval_batches", None - ) - self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) self.class_name_to_index = None self.index_to_class_name = None - def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: - """ - User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. - :raises ValueError: If self.class_names is not defined in the parent class. - - """ - - if not hasattr(self, "class_names"): - raise ValueError( - "Missing attribute: self.class_names " - "set self.freqai.class_names = [\"class a\", \"class b\", \"class c\"] " - "inside IStrategy.set_freqai_targets method." - ) - - self.init_class_names_to_index_mapping(self.class_names) - self.encode_classes_name(data_dictionary, dk) - n_features = data_dictionary["train_features"].shape[-1] - model = PyTorchMLPModel( - input_dim=n_features, - output_dim=len(self.class_names), - **self.model_kwargs - ) - model.to(self.device) - optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) - criterion = torch.nn.CrossEntropyLoss() - init_model = self.get_init_model(dk.pair) - trainer = PyTorchModelTrainer( - model=model, - optimizer=optimizer, - criterion=criterion, - model_meta_data={"class_names": self.class_names}, - device=self.device, - batch_size=self.batch_size, - max_iters=self.max_iters, - max_n_eval_batches=self.max_n_eval_batches, - init_model=init_model - ) - trainer.fit(data_dictionary) - return trainer - def predict( self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: @@ -97,7 +46,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): :pred_df: dataframe containing the predictions :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove data (NaNs) or felt uncertain about data (PCA and DI index) - :raises ValueError: if 'class_name' doesn't exist in model meta_data. + :raises ValueError: if 'class_names' doesn't exist in model meta_data. """ class_names = self.model.model_meta_data.get("class_names", None) @@ -106,7 +55,9 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): "Missing class names. " "self.model.model_meta_data[\"class_names\"] is None." ) - self.init_class_names_to_index_mapping(class_names) + + if not self.class_name_to_index: + self.init_class_names_to_index_mapping(class_names) dk.find_features(unfiltered_df) filtered_df, _ = dk.filter_features( @@ -116,49 +67,77 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): dk.data_dictionary["prediction_features"] = filtered_df self.data_cleaning_predict(dk) - dk.data_dictionary["prediction_features"] = torch.tensor( - dk.data_dictionary["prediction_features"].values - ).float().to(self.device) + x = torch.from_numpy(dk.data_dictionary["prediction_features"].values)\ + .float()\ + .to(self.device) - logits = self.model.model(dk.data_dictionary["prediction_features"]) + logits = self.model.model(x) probs = F.softmax(logits, dim=-1) predicted_classes = torch.argmax(probs, dim=-1) - predicted_classes_str = self.decode_classes_name(predicted_classes) + predicted_classes_str = self.decode_class_names(predicted_classes) pred_df_prob = DataFrame(probs.detach().numpy(), columns=class_names) pred_df = DataFrame(predicted_classes_str, columns=[dk.label_list[0]]) pred_df = pd.concat([pred_df, pred_df_prob], axis=1) return (pred_df, dk.do_predict) - def encode_classes_name(self, data_dictionary: Dict[str, pd.DataFrame], dk: FreqaiDataKitchen): + def encode_class_names( + self, + data_dictionary: Dict[str, pd.DataFrame], + dk: FreqaiDataKitchen, + class_names: List[str], + ): """ - encode class name str -> int - assuming first column of *_labels data frame to contain class names + encode class name, str -> int + assuming first column of *_labels data frame to be the target column + containing the class names """ target_column_name = dk.label_list[0] for split in ["train", "test"]: label_df = data_dictionary[f"{split}_labels"] - self.assert_valid_class_names(label_df[target_column_name]) + self.assert_valid_class_names(label_df[target_column_name], class_names) label_df[target_column_name] = list( map(lambda x: self.class_name_to_index[x], label_df[target_column_name]) ) - def assert_valid_class_names(self, labels: pd.Series): - non_defined_labels = set(labels) - set(self.class_names) + @staticmethod + def assert_valid_class_names( + target_column: pd.Series, + class_names: List[str] + ): + non_defined_labels = set(target_column) - set(class_names) if len(non_defined_labels) != 0: raise OperationalException( f"Found non defined labels: {non_defined_labels}, ", - f"expecting labels: {self.class_names}" + f"expecting labels: {class_names}" ) - def decode_classes_name(self, classes: torch.Tensor) -> List[str]: + def decode_class_names(self, class_ints: torch.Tensor) -> List[str]: """ - decode class name int -> str + decode class name, int -> str """ - return list(map(lambda x: self.index_to_class_name[x.item()], classes)) + return list(map(lambda x: self.index_to_class_name[x.item()], class_ints)) def init_class_names_to_index_mapping(self, class_names): self.class_name_to_index = {s: i for i, s in enumerate(class_names)} self.index_to_class_name = {i: s for i, s in enumerate(class_names)} - logger.info(f"class_name_to_index: {self.class_name_to_index}") + logger.info(f"encoded class name to index: {self.class_name_to_index}") + + def convert_label_column_to_int( + self, + data_dictionary: Dict[str, pd.DataFrame], + dk: FreqaiDataKitchen, + class_names: List[str] + ): + self.init_class_names_to_index_mapping(class_names) + self.encode_class_names(data_dictionary, dk, class_names) + + def get_class_names(self) -> List[str]: + if not hasattr(self, "class_names"): + raise ValueError( + "Missing attribute: self.class_names " + "set self.freqai.class_names = [\"class a\", \"class b\", \"class c\"] " + "inside IStrategy.set_freqai_targets method." + ) + return self.class_names diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index a5fe9b90b..181c0539d 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -88,10 +88,12 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'PyTorchClassifierMultiTarget' in model: model_save_ext = 'zip' freqai_conf['freqai']['model_training_parameters'].update({ - "max_iters": 1, - "batch_size": 64, "learning_rate": 3e-4, - "max_n_eval_batches": 1, + "trainer_kwargs": { + "max_iters": 1, + "batch_size": 64, + "max_n_eval_batches": 1, + }, "model_kwargs": { "hidden_dim": 32, "dropout_percent": 0.2, From 2a1a8c0e644b39b72b5a88eb78945162274d8bde Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 14:49:12 +0200 Subject: [PATCH 056/208] fix test --- tests/freqai/test_freqai_interface.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 181c0539d..33c18677b 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -85,22 +85,6 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'test_3ac' in model or 'test_4ac' in model: freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") - if 'PyTorchClassifierMultiTarget' in model: - model_save_ext = 'zip' - freqai_conf['freqai']['model_training_parameters'].update({ - "learning_rate": 3e-4, - "trainer_kwargs": { - "max_iters": 1, - "batch_size": 64, - "max_n_eval_batches": 1, - }, - "model_kwargs": { - "hidden_dim": 32, - "dropout_percent": 0.2, - "n_layer": 1, - } - }) - strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) @@ -184,7 +168,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'CatboostClassifier', 'XGBoostClassifier', 'XGBoostRFClassifier', - 'PyTorchClassifierMultiTarget', + 'PyTorchClassifier', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): if (is_arm() or is_py11()) and model == 'CatboostClassifier': From fbf7049ac5715e1d047fec1b4f99cb75f6efa1d7 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 15:09:50 +0200 Subject: [PATCH 057/208] sort imports --- freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py b/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py index d6be8c1df..2f6705311 100644 --- a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py +++ b/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py @@ -1,12 +1,12 @@ from typing import Any, Dict +import torch + from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.PyTorchClassifierClassifier import PyTorchClassifier from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel -import torch - class MLPPyTorchClassifier(PyTorchClassifier): """ From e08d8190ae80a1acf8aa1e8dd75891c78d134063 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 15:21:34 +0200 Subject: [PATCH 058/208] fix test --- tests/freqai/test_freqai_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 33c18677b..d183501ea 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -168,7 +168,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'CatboostClassifier', 'XGBoostClassifier', 'XGBoostRFClassifier', - 'PyTorchClassifier', + 'MLPPyTorchClassifier', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): if (is_arm() or is_py11()) and model == 'CatboostClassifier': From ddd1b5c0fffd233c77c976ac1396309fa189cfa8 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 17:03:36 +0200 Subject: [PATCH 059/208] modify feedforward net, move layer norm to start of thr block --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 91e496c5d..482b3f889 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -22,7 +22,7 @@ class PyTorchMLPModel(nn.Module): def forward(self, x: Tensor) -> Tensor: x = self.relu(self.input_layer(x)) x = self.dropout(x) - x = self.relu(self.blocks(x)) + x = self.blocks(x) logits = self.output_layer(x) return logits @@ -35,8 +35,8 @@ class Block(nn.Module): self.ln = nn.LayerNorm(hidden_dim) def forward(self, x): - x = self.dropout(self.ff(x)) - x = self.ln(x) + x = self.ff(self.ln(x)) + x = self.dropout(x) return x @@ -46,7 +46,6 @@ class FeedForward(nn.Module): self.net = nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), - nn.Linear(hidden_dim, hidden_dim), ) def forward(self, x): From ea08931ab3455fb3e083fc2ae65cacbce455fef4 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 17:45:30 +0200 Subject: [PATCH 060/208] add mlp documentation --- .../prediction_models/PyTorchMLPModel.py | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 482b3f889..07056e930 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -1,16 +1,42 @@ import logging import torch.nn as nn -from torch import Tensor - +import torch logger = logging.getLogger(__name__) class PyTorchMLPModel(nn.Module): + """ + A multi-layer perceptron (MLP) model implemented using PyTorch. + + :param input_dim: The number of input features. + :param output_dim: The number of output classes. + :param hidden_dim: The number of hidden units in each layer. Default: 256 + :param dropout_percent: The dropout rate for regularization. Default: 0.2 + :param n_layer: The number of layers in the MLP. Default: 1 + + :returns: The output of the MLP, with shape (batch_size, output_dim) + + + A neural network typically consists of input, output, and hidden layers, where the + information flows from the input layer through the hidden layers to the output layer. + In a feedforward neural network, also known as a multilayer perceptron (MLP), the + information flows in one direction only. Each hidden layer contains multiple units + or nodes that take input from the previous layer and produce output that goes to the + next layer. + + The hidden_dim parameter in the FeedForward class refers to the number of units + (or nodes) in the hidden layer. This parameter controls the complexity of the neural + network and determines how many nonlinear relationships the network can represent. + A higher value of hidden_dim allows the network to represent more complex functions + but may also make the network more prone to overfitting, where the model memorizes + the training data instead of learning general patterns. + """ + def __init__(self, input_dim: int, output_dim: int, **kwargs): super(PyTorchMLPModel, self).__init__() - hidden_dim: int = kwargs.get("hidden_dim", 1024) + hidden_dim: int = kwargs.get("hidden_dim", 256) dropout_percent: int = kwargs.get("dropout_percent", 0.2) n_layer: int = kwargs.get("n_layer", 1) self.input_layer = nn.Linear(input_dim, hidden_dim) @@ -19,7 +45,7 @@ class PyTorchMLPModel(nn.Module): self.relu = nn.ReLU() self.dropout = nn.Dropout(p=dropout_percent) - def forward(self, x: Tensor) -> Tensor: + def forward(self, x: torch.Tensor) -> torch.Tensor: x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.blocks(x) @@ -28,19 +54,35 @@ class PyTorchMLPModel(nn.Module): class Block(nn.Module): + """ + A building block for a multi-layer perceptron (MLP) implemented using PyTorch. + + :param hidden_dim: The number of hidden units in the feedforward network. + :param dropout_percent: The dropout rate for regularization. + + :returns: torch.Tensor. with shape (batch_size, hidden_dim) + """ + def __init__(self, hidden_dim: int, dropout_percent: int): super(Block, self).__init__() self.ff = FeedForward(hidden_dim) self.dropout = nn.Dropout(p=dropout_percent) self.ln = nn.LayerNorm(hidden_dim) - def forward(self, x): + def forward(self, x: torch.Tensor) -> torch.Tensor: x = self.ff(self.ln(x)) x = self.dropout(x) return x class FeedForward(nn.Module): + """ + A fully-connected feedforward neural network block. + + :param hidden_dim: The number of hidden units in the block. + :return: torch.Tensor. with shape (batch_size, hidden_dim) + """ + def __init__(self, hidden_dim: int): super(FeedForward, self).__init__() self.net = nn.Sequential( @@ -48,5 +90,5 @@ class FeedForward(nn.Module): nn.ReLU(), ) - def forward(self, x): + def forward(self, x: torch.Tensor) -> torch.Tensor: return self.net(x) From d04146d1b15cc35b5cac3223e23b05297b391986 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 18:04:01 +0200 Subject: [PATCH 061/208] improve mlp documentation --- .../prediction_models/PyTorchMLPModel.py | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 07056e930..c2b6c1b93 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -10,28 +10,25 @@ class PyTorchMLPModel(nn.Module): """ A multi-layer perceptron (MLP) model implemented using PyTorch. - :param input_dim: The number of input features. - :param output_dim: The number of output classes. - :param hidden_dim: The number of hidden units in each layer. Default: 256 - :param dropout_percent: The dropout rate for regularization. Default: 0.2 - :param n_layer: The number of layers in the MLP. Default: 1 + :param input_dim: The number of input features. This parameter specifies the number + of features in the input data that the MLP will use to make predictions. + :param output_dim: The number of output classes. This parameter specifies the number + of classes that the MLP will predict. + :param hidden_dim: The number of hidden units in each layer. This parameter controls + the complexity of the MLP and determines how many nonlinear relationships the MLP + can represent. Increasing the number of hidden units can increase the capacity of + the MLP to model complex patterns, but it also increases the risk of overfitting + the training data. Default: 256 + :param dropout_percent: The dropout rate for regularization. This parameter specifies + the probability of dropping out a neuron during training to prevent overfitting. + The dropout rate should be tuned carefully to balance between underfitting and + overfitting. Default: 0.2 + :param n_layer: The number of layers in the MLP. This parameter specifies the number + of layers in the MLP architecture. Adding more layers to the MLP can increase its + capacity to model complex patterns, but it also increases the risk of overfitting + the training data. Default: 1 :returns: The output of the MLP, with shape (batch_size, output_dim) - - - A neural network typically consists of input, output, and hidden layers, where the - information flows from the input layer through the hidden layers to the output layer. - In a feedforward neural network, also known as a multilayer perceptron (MLP), the - information flows in one direction only. Each hidden layer contains multiple units - or nodes that take input from the previous layer and produce output that goes to the - next layer. - - The hidden_dim parameter in the FeedForward class refers to the number of units - (or nodes) in the hidden layer. This parameter controls the complexity of the neural - network and determines how many nonlinear relationships the network can represent. - A higher value of hidden_dim allows the network to represent more complex functions - but may also make the network more prone to overfitting, where the model memorizes - the training data instead of learning general patterns. """ def __init__(self, input_dim: int, output_dim: int, **kwargs): @@ -55,7 +52,7 @@ class PyTorchMLPModel(nn.Module): class Block(nn.Module): """ - A building block for a multi-layer perceptron (MLP) implemented using PyTorch. + A building block for a multi-layer perceptron (MLP). :param hidden_dim: The number of hidden units in the feedforward network. :param dropout_percent: The dropout rate for regularization. @@ -77,7 +74,7 @@ class Block(nn.Module): class FeedForward(nn.Module): """ - A fully-connected feedforward neural network block. + A simple fully-connected feedforward neural network block. :param hidden_dim: The number of hidden units in the block. :return: torch.Tensor. with shape (batch_size, hidden_dim) From 501e746c52dfcd2eb7a54e1d0b1f45a0bdba2b0f Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 18:10:57 +0200 Subject: [PATCH 062/208] improve mlp documentation --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index c2b6c1b93..0e6b3c7bb 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -10,6 +10,9 @@ class PyTorchMLPModel(nn.Module): """ A multi-layer perceptron (MLP) model implemented using PyTorch. + This class mainly serves as a simple example for the integration of PyTorch model's + to freqai. It is not optimized at all and should not be used for production purposes. + :param input_dim: The number of input features. This parameter specifies the number of features in the input data that the MLP will use to make predictions. :param output_dim: The number of output classes. This parameter specifies the number From 601c37f862eea19c703ca73e2e619737c8754864 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 11:54:17 +0200 Subject: [PATCH 063/208] refactor classifiers class names --- .../{PyTorchClassifierClassifier.py => PyTorchClassifier.py} | 0 .../{MLPPyTorchClassifier.py => PyTorchMLPClassifier.py} | 4 ++-- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename freqtrade/freqai/prediction_models/{PyTorchClassifierClassifier.py => PyTorchClassifier.py} (100%) rename freqtrade/freqai/prediction_models/{MLPPyTorchClassifier.py => PyTorchMLPClassifier.py} (95%) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py b/freqtrade/freqai/prediction_models/PyTorchClassifier.py similarity index 100% rename from freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py rename to freqtrade/freqai/prediction_models/PyTorchClassifier.py diff --git a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py similarity index 95% rename from freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py rename to freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 2f6705311..453995ce8 100644 --- a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -4,11 +4,11 @@ import torch from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.PyTorchClassifierClassifier import PyTorchClassifier +from freqtrade.freqai.prediction_models.PyTorchClassifier import PyTorchClassifier from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel -class MLPPyTorchClassifier(PyTorchClassifier): +class PyTorchMLPClassifier(PyTorchClassifier): """ This class implements the fit method of IFreqaiModel. int the fit method we initialize the model and trainer objects. diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 0e6b3c7bb..f711a53a7 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -49,8 +49,8 @@ class PyTorchMLPModel(nn.Module): x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.blocks(x) - logits = self.output_layer(x) - return logits + x = self.output_layer(x) + return x class Block(nn.Module): From 54db239175643c9628f2c2278642b03b28cea6ee Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 17:06:33 +0200 Subject: [PATCH 064/208] add pytorch regressor example --- .../freqai/base_models/PyTorchModelTrainer.py | 10 ++- .../prediction_models/PyTorchClassifier.py | 10 --- .../prediction_models/PyTorchMLPClassifier.py | 3 +- .../prediction_models/PyTorchMLPRegressor.py | 78 +++++++++++++++++++ .../prediction_models/PyTorchRegressor.py | 50 ++++++++++++ 5 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py create mode 100644 freqtrade/freqai/prediction_models/PyTorchRegressor.py diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index f91b44924..63bf63c40 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Type import pandas as pd import torch @@ -20,6 +20,7 @@ class PyTorchModelTrainer: criterion: nn.Module, device: str, init_model: Dict, + target_tensor_type: torch.dtype, model_meta_data: Dict[str, Any] = {}, **kwargs ): @@ -30,6 +31,8 @@ class PyTorchModelTrainer: :param device: The device to use for training (e.g. 'cpu', 'cuda'). :param init_model: A dictionary containing the initial model/optimizer state_dict and model_meta_data saved by self.save() method. + :param target_tensor_type: type of target tensor, for classification usually + torch.long, for regressor usually torch.float. :param model_meta_data: Additional metadata about the model (optional). :param max_iters: The number of training iterations to run. iteration here refers to the number of times we call @@ -42,6 +45,7 @@ class PyTorchModelTrainer: self.criterion = criterion self.model_meta_data = model_meta_data self.device = device + self.target_tensor_type = target_tensor_type self.max_iters: int = kwargs.get("max_iters", 100) self.batch_size: int = kwargs.get("batch_size", 64) self.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None) @@ -123,8 +127,8 @@ class PyTorchModelTrainer: labels_view = labels_shape[0] if labels_shape[1] == 1 else labels_shape dataset = TensorDataset( torch.from_numpy(data_dictionary[f"{split}_features"].values).float(), - torch.from_numpy(data_dictionary[f"{split}_labels"].astype(float).values) - .long() + torch.from_numpy(data_dictionary[f"{split}_labels"].values) + .to(self.target_tensor_type) .view(labels_view) ) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifier.py b/freqtrade/freqai/prediction_models/PyTorchClassifier.py index 0be10b31e..01432e0fe 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifier.py @@ -22,16 +22,6 @@ class PyTorchClassifier(BasePyTorchModel): User must implement fit method """ def __init__(self, **kwargs): - """ - int: The number of nodes in the hidden layer of the neural network. - int: The maximum number of iterations to run during training. - int: The batch size to use during training. - float: The learning rate to use during training. - int: The number of training iterations between each evaluation. - dict: A dictionary mapping class names to their corresponding indices. - dict: A dictionary mapping indices to their corresponding class names. - """ - super().__init__(**kwargs) self.class_name_to_index = None self.index_to_class_name = None diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 453995ce8..ce8fbd336 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -11,7 +11,7 @@ from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel class PyTorchMLPClassifier(PyTorchClassifier): """ This class implements the fit method of IFreqaiModel. - int the fit method we initialize the model and trainer objects. + in the fit method we initialize the model and trainer objects. the only requirement from the model is to be aligned to PyTorchClassifier predict method that expects the model to predict tensor of type long. the trainer defines the training loop. @@ -75,6 +75,7 @@ class PyTorchMLPClassifier(PyTorchClassifier): model_meta_data={"class_names": class_names}, device=self.device, init_model=init_model, + target_tensor_type=torch.long, **self.trainer_kwargs, ) trainer.fit(data_dictionary) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py new file mode 100644 index 000000000..4685c332a --- /dev/null +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -0,0 +1,78 @@ +from typing import Any, Dict + +import torch + +from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel +from freqtrade.freqai.prediction_models.PyTorchRegressor import PyTorchRegressor + + +class PyTorchMLPRegressor(PyTorchRegressor): + """ + This class implements the fit method of IFreqaiModel. + in the fit method we initialize the model and trainer objects. + the only requirement from the model is to be aligned to PyTorchRegressor + predict method that expects the model to predict tensor of type float. + the trainer defines the training loop. + + parameters are passed via `model_training_parameters` under the freqai + section in the config file. e.g: + { + ... + "freqai": { + ... + "model_training_parameters" : { + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 5000, + "batch_size": 64, + "max_n_eval_batches": None, + }, + "model_kwargs": { + "hidden_dim": 512, + "dropout_percent": 0.2, + "n_layer": 1, + }, + } + } + } + + + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + model_training_params = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, any] = model_training_params.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + """ + + n_features = data_dictionary["train_features"].shape[-1] + model = PyTorchMLPModel( + input_dim=n_features, + output_dim=1, + **self.model_kwargs + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.MSELoss() + init_model = self.get_init_model(dk.pair) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + device=self.device, + init_model=init_model, + target_tensor_type=torch.float, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary) + return trainer diff --git a/freqtrade/freqai/prediction_models/PyTorchRegressor.py b/freqtrade/freqai/prediction_models/PyTorchRegressor.py new file mode 100644 index 000000000..837fbd836 --- /dev/null +++ b/freqtrade/freqai/prediction_models/PyTorchRegressor.py @@ -0,0 +1,50 @@ +import logging +from typing import Tuple + +import numpy as np +import numpy.typing as npt +import torch +from pandas import DataFrame + +from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen + + +logger = logging.getLogger(__name__) + + +class PyTorchRegressor(BasePyTorchModel): + """ + A PyTorch implementation of a regressor. + User must implement fit method + """ + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def predict( + self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs + ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: + """ + Filter the prediction features data and predict with it. + :param unfiltered_df: Full dataframe for the current backtest period. + :return: + :pred_df: dataframe containing the predictions + :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove + data (NaNs) or felt uncertain about data (PCA and DI index) + """ + + dk.find_features(unfiltered_df) + filtered_df, _ = dk.filter_features( + unfiltered_df, dk.training_features_list, training_filter=False + ) + filtered_df = dk.normalize_data_from_metadata(filtered_df) + dk.data_dictionary["prediction_features"] = filtered_df + + self.data_cleaning_predict(dk) + x = torch.from_numpy(dk.data_dictionary["prediction_features"].values)\ + .float()\ + .to(self.device) + + y = self.model.model(x) + pred_df = DataFrame(y.detach().numpy(), columns=[dk.label_list[0]]) + return (pred_df, dk.do_predict) \ No newline at end of file From f659f8e309dbeec31f313d2ef0e0d54c266100c7 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 17:52:52 +0200 Subject: [PATCH 065/208] remove unused imports --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 63bf63c40..8097b8b85 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Any, Dict, Optional, Type +from typing import Any, Dict, Optional import pandas as pd import torch From d98890f32e2a866c9a9934858998ebd163c88aed Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 17:55:05 +0200 Subject: [PATCH 066/208] sort imports --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index f711a53a7..c43b06685 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -1,7 +1,7 @@ import logging -import torch.nn as nn import torch +import torch.nn as nn logger = logging.getLogger(__name__) From 9aec1ddb17f03b62a9dc5781eb76d182dfa7c1f4 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 17:56:51 +0200 Subject: [PATCH 067/208] sort imports --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index c43b06685..a9f609e8e 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -3,6 +3,7 @@ import logging import torch import torch.nn as nn + logger = logging.getLogger(__name__) From c00ffcee59e60f3218b5e09258ad15d57a439ead Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 18:04:02 +0200 Subject: [PATCH 068/208] fix pytorch classifier test --- tests/freqai/test_freqai_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index d183501ea..01aa0d1db 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -168,7 +168,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'CatboostClassifier', 'XGBoostClassifier', 'XGBoostRFClassifier', - 'MLPPyTorchClassifier', + 'PyTorchMLPClassifier', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): if (is_arm() or is_py11()) and model == 'CatboostClassifier': From 68728409aa5c7b668fbb1228cfed117901a7f1a9 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 18:04:14 +0200 Subject: [PATCH 069/208] add pytorch regressor test --- tests/freqai/test_freqai_interface.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 01aa0d1db..3407a5a95 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -48,11 +48,12 @@ def can_run_model(model: str) -> None: ('XGBoostRegressor', False, True, False, True, False, 10), ('XGBoostRFRegressor', False, False, False, True, False, 0), ('CatboostRegressor', False, False, False, True, True, 0), + ('MLPPyTorchRegressor', False, False, False, True, False, 0), ('ReinforcementLearner', False, True, False, True, False, 0), ('ReinforcementLearner_multiproc', False, False, False, True, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, False, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, True, False, 0), - ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0) + ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0), ]) def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, dbscan, float32, can_short, shuffle, buffer): @@ -85,6 +86,9 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'test_3ac' in model or 'test_4ac' in model: freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") + if 'MLPPyTorchRegressor' in model: + model_save_ext = 'zip' + strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) From 0510cf44910d4980eb66ecf2a5f6947607a7a8f9 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 18:08:38 +0200 Subject: [PATCH 070/208] add config params to tests --- tests/freqai/test_freqai_interface.py | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 3407a5a95..d35b00013 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -88,6 +88,19 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'MLPPyTorchRegressor' in model: model_save_ext = 'zip' + freqai_conf['freqai']['model_training_parameters'].update({ + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 1, + "batch_size": 64, + "max_n_eval_batches": 1, + }, + "model_kwargs": { + "hidden_dim": 32, + "dropout_percent": 0.2, + "n_layer": 1, + } + }) strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) @@ -200,6 +213,23 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + + if 'MLPPyTorchClassifier': + freqai_conf['freqai']['model_training_parameters'].update({ + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 1, + "batch_size": 64, + "max_n_eval_batches": 1, + }, + "model_kwargs": { + "hidden_dim": 32, + "dropout_percent": 0.2, + "n_layer": 1, + } + }) + + if freqai.dd.model_type == 'joblib': model_file_extension = ".joblib" elif freqai.dd.model_type == "pytorch": From 81a2cbb4eb65c7b2a7df3b3da93a0b5d7c87800a Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 18:10:17 +0200 Subject: [PATCH 071/208] fix tests --- tests/freqai/test_freqai_interface.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index d35b00013..7931dc7a4 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -48,7 +48,7 @@ def can_run_model(model: str) -> None: ('XGBoostRegressor', False, True, False, True, False, 10), ('XGBoostRFRegressor', False, False, False, True, False, 0), ('CatboostRegressor', False, False, False, True, True, 0), - ('MLPPyTorchRegressor', False, False, False, True, False, 0), + ('PyTorchMLPRegressor', False, False, False, True, False, 0), ('ReinforcementLearner', False, True, False, True, False, 0), ('ReinforcementLearner_multiproc', False, False, False, True, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, False, False, 0), @@ -86,7 +86,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'test_3ac' in model or 'test_4ac' in model: freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") - if 'MLPPyTorchRegressor' in model: + if 'PyTorchMLPRegressor' in model: model_save_ext = 'zip' freqai_conf['freqai']['model_training_parameters'].update({ "learning_rate": 3e-4, @@ -214,7 +214,7 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) - if 'MLPPyTorchClassifier': + if 'PyTorchMLPClassifier': freqai_conf['freqai']['model_training_parameters'].update({ "learning_rate": 3e-4, "trainer_kwargs": { From 500c401b759b7cac45d2c54ff5d74ed4aab3221c Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 18:39:50 +0200 Subject: [PATCH 072/208] improve pytorch classifier documentation --- .../freqai/prediction_models/PyTorchClassifier.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifier.py b/freqtrade/freqai/prediction_models/PyTorchClassifier.py index 01432e0fe..b14a89b38 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifier.py @@ -20,6 +20,18 @@ class PyTorchClassifier(BasePyTorchModel): """ A PyTorch implementation of a classifier. User must implement fit method + + Important! + User must declare the target class names in the strategy, under + IStrategy.set_freqai_targets method. + ``` + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): + self.freqai.class_names = ["down", "up"] + dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) > + dataframe["close"], 'up', 'down') + + return dataframe + ``` """ def __init__(self, **kwargs): super().__init__(**kwargs) @@ -127,7 +139,7 @@ class PyTorchClassifier(BasePyTorchModel): if not hasattr(self, "class_names"): raise ValueError( "Missing attribute: self.class_names " - "set self.freqai.class_names = [\"class a\", \"class b\", \"class c\"] " + "set self.freqai.class_names = ['class a', 'class b', 'class c'] " "inside IStrategy.set_freqai_targets method." ) return self.class_names From 6b4d9f97c13472267c5b6d7ef920eafd8001acb3 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 19:28:30 +0200 Subject: [PATCH 073/208] clean code --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 6 +++--- freqtrade/freqai/prediction_models/PyTorchRegressor.py | 2 +- tests/freqai/test_freqai_interface.py | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index a9f609e8e..22fb9c3f0 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -36,7 +36,7 @@ class PyTorchMLPModel(nn.Module): """ def __init__(self, input_dim: int, output_dim: int, **kwargs): - super(PyTorchMLPModel, self).__init__() + super().__init__() hidden_dim: int = kwargs.get("hidden_dim", 256) dropout_percent: int = kwargs.get("dropout_percent", 0.2) n_layer: int = kwargs.get("n_layer", 1) @@ -65,7 +65,7 @@ class Block(nn.Module): """ def __init__(self, hidden_dim: int, dropout_percent: int): - super(Block, self).__init__() + super().__init__() self.ff = FeedForward(hidden_dim) self.dropout = nn.Dropout(p=dropout_percent) self.ln = nn.LayerNorm(hidden_dim) @@ -85,7 +85,7 @@ class FeedForward(nn.Module): """ def __init__(self, hidden_dim: int): - super(FeedForward, self).__init__() + super().__init__() self.net = nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), diff --git a/freqtrade/freqai/prediction_models/PyTorchRegressor.py b/freqtrade/freqai/prediction_models/PyTorchRegressor.py index 837fbd836..440db96b9 100644 --- a/freqtrade/freqai/prediction_models/PyTorchRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchRegressor.py @@ -47,4 +47,4 @@ class PyTorchRegressor(BasePyTorchModel): y = self.model.model(x) pred_df = DataFrame(y.detach().numpy(), columns=[dk.label_list[0]]) - return (pred_df, dk.do_predict) \ No newline at end of file + return (pred_df, dk.do_predict) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 7931dc7a4..c1d9998d6 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -229,7 +229,6 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): } }) - if freqai.dd.model_type == 'joblib': model_file_extension = ".joblib" elif freqai.dd.model_type == "pytorch": From 0a55753faf54fb3e7e741041617de6a44c9e37aa Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 19:40:36 +0200 Subject: [PATCH 074/208] move default attributes of pytorch classifier to initializer, to prevent mypy from complaining --- .../prediction_models/PyTorchMLPClassifier.py | 16 +++++++++++----- .../prediction_models/PyTorchMLPRegressor.py | 16 +++++++++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index ce8fbd336..edba75c2a 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -41,12 +41,18 @@ class PyTorchMLPClassifier(PyTorchClassifier): """ - def __init__(self, **kwargs): + def __init__( + self, + learning_rate: float = 3e-4, + model_kwargs: Dict[str, Any] = {}, + trainer_kwargs: Dict[str, Any] = {}, + **kwargs + ): super().__init__(**kwargs) - model_training_params = self.freqai_info.get("model_training_parameters", {}) - self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) - self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) - self.trainer_kwargs: Dict[str, any] = model_training_params.get("trainer_kwargs", {}) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", learning_rate) + self.model_kwargs: Dict[str, any] = config.get("model_kwargs", model_kwargs) + self.trainer_kwargs: Dict[str, any] = config.get("trainer_kwargs", trainer_kwargs) def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 4685c332a..2118c27e1 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -41,12 +41,18 @@ class PyTorchMLPRegressor(PyTorchRegressor): """ - def __init__(self, **kwargs): + def __init__( + self, + learning_rate: float = 3e-4, + model_kwargs: Dict[str, Any] = {}, + trainer_kwargs: Dict[str, Any] = {}, + **kwargs + ): super().__init__(**kwargs) - model_training_params = self.freqai_info.get("model_training_parameters", {}) - self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) - self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) - self.trainer_kwargs: Dict[str, any] = model_training_params.get("trainer_kwargs", {}) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", learning_rate) + self.model_kwargs: Dict[str, any] = config.get("model_kwargs", model_kwargs) + self.trainer_kwargs: Dict[str, any] = config.get("trainer_kwargs", trainer_kwargs) def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ From c06cd38951e5fbdb08c805cf573ff2bb4877a5cd Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 19:55:39 +0200 Subject: [PATCH 075/208] clean code --- freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 2118c27e1..06092c5a0 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -37,8 +37,6 @@ class PyTorchMLPRegressor(PyTorchRegressor): } } } - - """ def __init__( From a4b617e4824f633a03a65c4d6541f2e7587a7883 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 20:22:28 +0200 Subject: [PATCH 076/208] type hints fixes --- .../freqai/prediction_models/PyTorchMLPClassifier.py | 11 ++++------- .../freqai/prediction_models/PyTorchMLPRegressor.py | 4 ++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index edba75c2a..6b7d9c034 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -13,8 +13,7 @@ class PyTorchMLPClassifier(PyTorchClassifier): This class implements the fit method of IFreqaiModel. in the fit method we initialize the model and trainer objects. the only requirement from the model is to be aligned to PyTorchClassifier - predict method that expects the model to predict tensor of type long. - the trainer defines the training loop. + predict method that expects the model to predict a tensor of type long. parameters are passed via `model_training_parameters` under the freqai section in the config file. e.g: @@ -37,8 +36,6 @@ class PyTorchMLPClassifier(PyTorchClassifier): } } } - - """ def __init__( @@ -51,15 +48,15 @@ class PyTorchMLPClassifier(PyTorchClassifier): super().__init__(**kwargs) config = self.freqai_info.get("model_training_parameters", {}) self.learning_rate: float = config.get("learning_rate", learning_rate) - self.model_kwargs: Dict[str, any] = config.get("model_kwargs", model_kwargs) - self.trainer_kwargs: Dict[str, any] = config.get("trainer_kwargs", trainer_kwargs) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", model_kwargs) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", trainer_kwargs) def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold all the training and test data/labels. - :raises ValueError: If self.class_names is not defined in the parent class. + :raises ValueError: If self.class_names is empty. """ class_names = self.get_class_names() diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 06092c5a0..16e7c0e79 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -49,8 +49,8 @@ class PyTorchMLPRegressor(PyTorchRegressor): super().__init__(**kwargs) config = self.freqai_info.get("model_training_parameters", {}) self.learning_rate: float = config.get("learning_rate", learning_rate) - self.model_kwargs: Dict[str, any] = config.get("model_kwargs", model_kwargs) - self.trainer_kwargs: Dict[str, any] = config.get("trainer_kwargs", trainer_kwargs) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", model_kwargs) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", trainer_kwargs) def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ From e8f040bfbd37108b50dab712716a5abc1ccfc2ec Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 20:38:43 +0200 Subject: [PATCH 077/208] add class_name attribute to freqai interface --- freqtrade/freqai/freqai_interface.py | 1 + .../freqai/prediction_models/PyTorchClassifier.py | 15 +++++++++------ .../prediction_models/PyTorchMLPClassifier.py | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 8a1ac436b..470ae1911 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -83,6 +83,7 @@ class IFreqaiModel(ABC): self.CONV_WIDTH = self.freqai_info.get('conv_width', 1) if self.ft_params.get("inlier_metric_window", 0): self.CONV_WIDTH = self.ft_params.get("inlier_metric_window", 0) * 2 + self.class_names: List[str] = [] # used in classification children classes self.pair_it = 0 self.pair_it_train = 0 self.total_pairs = len(self.config.get("exchange", {}).get("pair_whitelist")) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifier.py b/freqtrade/freqai/prediction_models/PyTorchClassifier.py index b14a89b38..e47021a55 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifier.py @@ -22,8 +22,11 @@ class PyTorchClassifier(BasePyTorchModel): User must implement fit method Important! - User must declare the target class names in the strategy, under - IStrategy.set_freqai_targets method. + + - User must declare the target class names in the strategy, + under IStrategy.set_freqai_targets method. + + for example, in your strategy: ``` def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): self.freqai.class_names = ["down", "up"] @@ -31,7 +34,6 @@ class PyTorchClassifier(BasePyTorchModel): dataframe["close"], 'up', 'down') return dataframe - ``` """ def __init__(self, **kwargs): super().__init__(**kwargs) @@ -55,7 +57,7 @@ class PyTorchClassifier(BasePyTorchModel): if not class_names: raise ValueError( "Missing class names. " - "self.model.model_meta_data[\"class_names\"] is None." + "self.model.model_meta_data['class_names'] is None." ) if not self.class_name_to_index: @@ -136,10 +138,11 @@ class PyTorchClassifier(BasePyTorchModel): self.encode_class_names(data_dictionary, dk, class_names) def get_class_names(self) -> List[str]: - if not hasattr(self, "class_names"): + if not self.class_names: raise ValueError( - "Missing attribute: self.class_names " + "self.class_names is empty, " "set self.freqai.class_names = ['class a', 'class b', 'class c'] " "inside IStrategy.set_freqai_targets method." ) + return self.class_names diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 6b7d9c034..373b81a82 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -56,7 +56,7 @@ class PyTorchMLPClassifier(PyTorchClassifier): User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold all the training and test data/labels. - :raises ValueError: If self.class_names is empty. + :raises ValueError: If self.class_names is not defined in the parent class. """ class_names = self.get_class_names() From 9906e7d646f980ebbed60e4f9501042ac802750e Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 11:23:45 +0200 Subject: [PATCH 078/208] clean code --- .../prediction_models/PyTorchMLPClassifier.py | 14 ++++---------- .../prediction_models/PyTorchMLPRegressor.py | 14 ++++---------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 373b81a82..e26b8b52c 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -38,18 +38,12 @@ class PyTorchMLPClassifier(PyTorchClassifier): } """ - def __init__( - self, - learning_rate: float = 3e-4, - model_kwargs: Dict[str, Any] = {}, - trainer_kwargs: Dict[str, Any] = {}, - **kwargs - ): + def __init__(self, **kwargs): super().__init__(**kwargs) config = self.freqai_info.get("model_training_parameters", {}) - self.learning_rate: float = config.get("learning_rate", learning_rate) - self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", model_kwargs) - self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", trainer_kwargs) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 16e7c0e79..94c0dfe46 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -39,18 +39,12 @@ class PyTorchMLPRegressor(PyTorchRegressor): } """ - def __init__( - self, - learning_rate: float = 3e-4, - model_kwargs: Dict[str, Any] = {}, - trainer_kwargs: Dict[str, Any] = {}, - **kwargs - ): + def __init__(self, **kwargs): super().__init__(**kwargs) config = self.freqai_info.get("model_training_parameters", {}) - self.learning_rate: float = config.get("learning_rate", learning_rate) - self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", model_kwargs) - self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", trainer_kwargs) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ From 443263803ce332bc6aed38a1502594a31af9c49a Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 11:42:05 +0200 Subject: [PATCH 079/208] unsqueeze target tensor when 1 dimensional --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 8097b8b85..52e6d5138 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -124,7 +124,7 @@ class PyTorchModelTrainer: data_loader_dictionary = {} for split in ["train", "test"]: labels_shape = data_dictionary[f"{split}_labels"].shape - labels_view = labels_shape[0] if labels_shape[1] == 1 else labels_shape + labels_view = (labels_shape[0], 1) if labels_shape[1] == 1 else labels_shape dataset = TensorDataset( torch.from_numpy(data_dictionary[f"{split}_features"].values).float(), torch.from_numpy(data_dictionary[f"{split}_labels"].values) From 97339e14cf63b116ef86da123ad52b1bb735ef54 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 12:29:05 +0200 Subject: [PATCH 080/208] round up divisions in calc_n_epochs --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 52e6d5138..6a4b128e3 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -1,4 +1,5 @@ import logging +import math from pathlib import Path from typing import Any, Dict, Optional @@ -148,10 +149,13 @@ class PyTorchModelTrainer: """ Calculates the number of epochs required to reach the maximum number of iterations specified in the model training parameters. + + the motivation here is that `max_iters` is easier to optimize and keep stable, + across different n_obs - the number of data points. """ - n_batches = n_obs // batch_size - epochs = n_iters // n_batches + n_batches = math.ceil(n_obs // batch_size) + epochs = math.ceil(n_iters // n_batches) return epochs def save(self, path: Path): From a80afc8f1b930334486c13b6836bf81fea978708 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 13:20:54 +0200 Subject: [PATCH 081/208] add optional target tensor squeezing to pytorch trainer --- .../freqai/base_models/PyTorchModelTrainer.py | 18 +++++++++++------- .../prediction_models/PyTorchMLPClassifier.py | 1 + 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 6a4b128e3..2ef4b57c9 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -22,6 +22,7 @@ class PyTorchModelTrainer: device: str, init_model: Dict, target_tensor_type: torch.dtype, + squeeze_target_tensor: bool = False, model_meta_data: Dict[str, Any] = {}, **kwargs ): @@ -35,11 +36,14 @@ class PyTorchModelTrainer: :param target_tensor_type: type of target tensor, for classification usually torch.long, for regressor usually torch.float. :param model_meta_data: Additional metadata about the model (optional). + :param squeeze_target_tensor: controls the target shape, used for loss functions + that requires 0D or 1D. :param max_iters: The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs. :param batch_size: The size of the batches to use during training. :param max_n_eval_batches: The maximum number batches to use for evaluation. + """ self.model = model self.optimizer = optimizer @@ -50,6 +54,7 @@ class PyTorchModelTrainer: self.max_iters: int = kwargs.get("max_iters", 100) self.batch_size: int = kwargs.get("batch_size", 64) self.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None) + self.squeeze_target_tensor = squeeze_target_tensor if init_model: self.load_from_checkpoint(init_model) @@ -124,15 +129,14 @@ class PyTorchModelTrainer: """ data_loader_dictionary = {} for split in ["train", "test"]: - labels_shape = data_dictionary[f"{split}_labels"].shape - labels_view = (labels_shape[0], 1) if labels_shape[1] == 1 else labels_shape - dataset = TensorDataset( - torch.from_numpy(data_dictionary[f"{split}_features"].values).float(), - torch.from_numpy(data_dictionary[f"{split}_labels"].values) + x = torch.from_numpy(data_dictionary[f"{split}_features"].values).float() + y = torch.from_numpy(data_dictionary[f"{split}_labels"].values)\ .to(self.target_tensor_type) - .view(labels_view) - ) + if self.squeeze_target_tensor: + y = y.squeeze() + + dataset = TensorDataset(x, y) data_loader = DataLoader( dataset, batch_size=self.batch_size, diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index e26b8b52c..b8f2df28b 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -73,6 +73,7 @@ class PyTorchMLPClassifier(PyTorchClassifier): device=self.device, init_model=init_model, target_tensor_type=torch.long, + squeeze_target_tensor=True, **self.trainer_kwargs, ) trainer.fit(data_dictionary) From 3fa23860c01747a572f81f37cc6bc6dbae4640ac Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 14:34:27 +0200 Subject: [PATCH 082/208] skip pytorch tests on python 3.11 and intel based mac os --- tests/freqai/test_freqai_interface.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index c1d9998d6..718d80f44 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -34,13 +34,14 @@ def is_mac() -> bool: def can_run_model(model: str) -> None: if (is_arm() or is_py11()) and "Catboost" in model: - pytest.skip("CatBoost is not supported on ARM") + pytest.skip("CatBoost is not supported on ARM.") - if is_mac() and not is_arm() and 'Reinforcement' in model: - pytest.skip("Reinforcement learning module not available on intel based Mac OS") + is_pytorch_model = 'Reinforcement' in model or 'PyTorch' in model + if is_pytorch_model and is_mac() and not is_arm(): + pytest.skip("Reinforcement learning / PyTorch module not available on intel based Mac OS.") - if is_py11() and 'Reinforcement' in model: - pytest.skip("Reinforcement learning currently not available on python 3.11.") + if is_pytorch_model and is_py11(): + pytest.skip("Reinforcement learning / PyTorch currently not available on python 3.11.") @pytest.mark.parametrize('model, pca, dbscan, float32, can_short, shuffle, buffer', [ From eba82360fac3776bcaed85e44d66e58292fa02df Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 15:18:05 +0200 Subject: [PATCH 083/208] skip pytorch tests on python 3.11 and intel based mac os --- tests/freqai/test_freqai_interface.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 718d80f44..b4d808af2 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -146,8 +146,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, ('CatboostClassifierMultiTarget', "freqai_test_multimodel_classifier_strat") ]) def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, strat): - if (is_arm() or is_py11()) and 'Catboost' in model: - pytest.skip("CatBoost is not supported on ARM") + can_run_model(model) freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"strategy": strat}) @@ -189,8 +188,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'PyTorchMLPClassifier', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): - if (is_arm() or is_py11()) and model == 'CatboostClassifier': - pytest.skip("CatBoost is not supported on ARM") + can_run_model(model) freqai_conf.update({"freqaimodel": model}) freqai_conf.update({"strategy": "freqai_test_classifier"}) From 83a7d888bc4ec4f13a92a6f80c20a8c4f8a8b603 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 15:19:34 +0200 Subject: [PATCH 084/208] type hint init in pytorch mlp classes --- freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py | 2 +- freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index b8f2df28b..f9214d410 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -38,7 +38,7 @@ class PyTorchMLPClassifier(PyTorchClassifier): } """ - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) config = self.freqai_info.get("model_training_parameters", {}) self.learning_rate: float = config.get("learning_rate", 3e-4) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 94c0dfe46..20417736c 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -39,7 +39,7 @@ class PyTorchMLPRegressor(PyTorchRegressor): } """ - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) config = self.freqai_info.get("model_training_parameters", {}) self.learning_rate: float = config.get("learning_rate", 3e-4) From 1ba01746a0993d1f826ddceaa99ce4bb2370f205 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 21 Mar 2023 15:09:54 +0100 Subject: [PATCH 085/208] organize pytorch files --- .../BaseTorchClassifier.py} | 2 +- .../BaseTorchRegressor.py} | 2 +- .../freqai/prediction_models/PyTorchMLPClassifier.py | 8 ++++---- freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py | 8 ++++---- .../{prediction_models => torch}/PyTorchMLPModel.py | 0 .../freqai/{base_models => torch}/PyTorchModelTrainer.py | 0 freqtrade/freqai/torch/__init__.py | 0 7 files changed, 10 insertions(+), 10 deletions(-) rename freqtrade/freqai/{prediction_models/PyTorchClassifier.py => base_models/BaseTorchClassifier.py} (99%) rename freqtrade/freqai/{prediction_models/PyTorchRegressor.py => base_models/BaseTorchRegressor.py} (97%) rename freqtrade/freqai/{prediction_models => torch}/PyTorchMLPModel.py (100%) rename freqtrade/freqai/{base_models => torch}/PyTorchModelTrainer.py (100%) create mode 100644 freqtrade/freqai/torch/__init__.py diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifier.py b/freqtrade/freqai/base_models/BaseTorchClassifier.py similarity index 99% rename from freqtrade/freqai/prediction_models/PyTorchClassifier.py rename to freqtrade/freqai/base_models/BaseTorchClassifier.py index e47021a55..1cfd742db 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifier.py +++ b/freqtrade/freqai/base_models/BaseTorchClassifier.py @@ -16,7 +16,7 @@ from freqtrade.freqai.data_kitchen import FreqaiDataKitchen logger = logging.getLogger(__name__) -class PyTorchClassifier(BasePyTorchModel): +class BaseTorchClassifier(BasePyTorchModel): """ A PyTorch implementation of a classifier. User must implement fit method diff --git a/freqtrade/freqai/prediction_models/PyTorchRegressor.py b/freqtrade/freqai/base_models/BaseTorchRegressor.py similarity index 97% rename from freqtrade/freqai/prediction_models/PyTorchRegressor.py rename to freqtrade/freqai/base_models/BaseTorchRegressor.py index 440db96b9..baaf097ee 100644 --- a/freqtrade/freqai/prediction_models/PyTorchRegressor.py +++ b/freqtrade/freqai/base_models/BaseTorchRegressor.py @@ -13,7 +13,7 @@ from freqtrade.freqai.data_kitchen import FreqaiDataKitchen logger = logging.getLogger(__name__) -class PyTorchRegressor(BasePyTorchModel): +class BaseTorchRegressor(BasePyTorchModel): """ A PyTorch implementation of a regressor. User must implement fit method diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index f9214d410..16866859b 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -2,13 +2,13 @@ from typing import Any, Dict import torch -from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer +from freqtrade.freqai.base_models.BaseTorchClassifier import BaseTorchClassifier from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.PyTorchClassifier import PyTorchClassifier -from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel +from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel +from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer -class PyTorchMLPClassifier(PyTorchClassifier): +class PyTorchMLPClassifier(BaseTorchClassifier): """ This class implements the fit method of IFreqaiModel. in the fit method we initialize the model and trainer objects. diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 20417736c..861d90a21 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -2,13 +2,13 @@ from typing import Any, Dict import torch -from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer +from freqtrade.freqai.base_models.BaseTorchRegressor import BaseTorchRegressor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel -from freqtrade.freqai.prediction_models.PyTorchRegressor import PyTorchRegressor +from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel +from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer -class PyTorchMLPRegressor(PyTorchRegressor): +class PyTorchMLPRegressor(BaseTorchRegressor): """ This class implements the fit method of IFreqaiModel. in the fit method we initialize the model and trainer objects. diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/torch/PyTorchMLPModel.py similarity index 100% rename from freqtrade/freqai/prediction_models/PyTorchMLPModel.py rename to freqtrade/freqai/torch/PyTorchMLPModel.py diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py similarity index 100% rename from freqtrade/freqai/base_models/PyTorchModelTrainer.py rename to freqtrade/freqai/torch/PyTorchModelTrainer.py diff --git a/freqtrade/freqai/torch/__init__.py b/freqtrade/freqai/torch/__init__.py new file mode 100644 index 000000000..e69de29bb From 02bccd0097aa442b46a4b4eb818bfba138057401 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 16:20:35 +0200 Subject: [PATCH 086/208] add pytorch mlp models to test_start_backtesting --- tests/freqai/conftest.py | 16 +++++++++++ tests/freqai/test_freqai_interface.py | 38 ++++++++------------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index 68e7ea49a..02cfdd882 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -83,6 +83,22 @@ def make_rl_config(conf): return conf +def mock_pytorch_mlp_model_training_parameters(conf): + return { + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 1, + "batch_size": 64, + "max_n_eval_batches": 1, + }, + "model_kwargs": { + "hidden_dim": 32, + "dropout_percent": 0.2, + "n_layer": 1, + } + } + + def get_patched_data_kitchen(mocker, freqaiconf): dk = FreqaiDataKitchen(freqaiconf) return dk diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index b4d808af2..5b460cda1 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -89,19 +89,8 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'PyTorchMLPRegressor' in model: model_save_ext = 'zip' - freqai_conf['freqai']['model_training_parameters'].update({ - "learning_rate": 3e-4, - "trainer_kwargs": { - "max_iters": 1, - "batch_size": 64, - "max_n_eval_batches": 1, - }, - "model_kwargs": { - "hidden_dim": 32, - "dropout_percent": 0.2, - "n_layer": 1, - } - }) + pytorch_mlp_mtp = mock_pytorch_mlp_model_training_parameters() + freqai_conf['freqai']['model_training_parameters'].update(pytorch_mlp_mtp) strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) @@ -214,19 +203,8 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): strategy, freqai.dk, data_load_timerange) if 'PyTorchMLPClassifier': - freqai_conf['freqai']['model_training_parameters'].update({ - "learning_rate": 3e-4, - "trainer_kwargs": { - "max_iters": 1, - "batch_size": 64, - "max_n_eval_batches": 1, - }, - "model_kwargs": { - "hidden_dim": 32, - "dropout_percent": 0.2, - "n_layer": 1, - } - }) + pytorch_mlp_mtp = mock_pytorch_mlp_model_training_parameters() + freqai_conf['freqai']['model_training_parameters'].update(pytorch_mlp_mtp) if freqai.dd.model_type == 'joblib': model_file_extension = ".joblib" @@ -251,10 +229,12 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): ("LightGBMRegressor", 2, "freqai_test_strat"), ("XGBoostRegressor", 2, "freqai_test_strat"), ("CatboostRegressor", 2, "freqai_test_strat"), + ("PyTorchMLPRegressor", 2, "freqai_test_strat"), ("ReinforcementLearner", 3, "freqai_rl_test_strat"), ("XGBoostClassifier", 2, "freqai_test_classifier"), ("LightGBMClassifier", 2, "freqai_test_classifier"), - ("CatboostClassifier", 2, "freqai_test_classifier") + ("CatboostClassifier", 2, "freqai_test_classifier"), + ("PyTorchMLPClassifier", 2, "freqai_test_classifier") ], ) def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog): @@ -275,6 +255,10 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog) if 'test_4ac' in model: freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") + if 'PyTorchMLP' in model: + pytorch_mlp_mtp = mock_pytorch_mlp_model_training_parameters() + freqai_conf['freqai']['model_training_parameters'].update(pytorch_mlp_mtp) + freqai_conf.get("freqai", {}).get("feature_parameters", {}).update( {"indicator_periods_candles": [2]}) From b9c7d338b39e7a28bf3536bf219b1798c9b5d4e8 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 16:38:05 +0200 Subject: [PATCH 087/208] fix test_start_backtesting --- tests/freqai/conftest.py | 3 ++- tests/freqai/test_freqai_interface.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index 02cfdd882..0f29301d0 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -1,5 +1,6 @@ from copy import deepcopy from pathlib import Path +from typing import Any, Dict from unittest.mock import MagicMock import pytest @@ -83,7 +84,7 @@ def make_rl_config(conf): return conf -def mock_pytorch_mlp_model_training_parameters(conf): +def mock_pytorch_mlp_model_training_parameters() -> Dict[str, Any]: return { "learning_rate": 3e-4, "trainer_kwargs": { diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 5b460cda1..8b126fe55 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -15,7 +15,8 @@ from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import Trade from freqtrade.plugins.pairlistmanager import PairListManager from tests.conftest import EXMS, create_mock_trades, get_patched_exchange, log_has_re -from tests.freqai.conftest import get_patched_freqai_strategy, make_rl_config +from tests.freqai.conftest import get_patched_freqai_strategy, make_rl_config, \ + mock_pytorch_mlp_model_training_parameters def is_py11() -> bool: From f81e3d86677ff0f1549983f3665b2d8269014ca1 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 16:42:13 +0200 Subject: [PATCH 088/208] sort imports --- tests/freqai/test_freqai_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 8b126fe55..ffac1e248 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -15,8 +15,8 @@ from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import Trade from freqtrade.plugins.pairlistmanager import PairListManager from tests.conftest import EXMS, create_mock_trades, get_patched_exchange, log_has_re -from tests.freqai.conftest import get_patched_freqai_strategy, make_rl_config, \ - mock_pytorch_mlp_model_training_parameters +from tests.freqai.conftest import (get_patched_freqai_strategy, make_rl_config, + mock_pytorch_mlp_model_training_parameters) def is_py11() -> bool: From 479aafc331410d2533e8ae7e3cd1e8554890ac38 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 22 Mar 2023 17:50:00 +0200 Subject: [PATCH 089/208] rename Torch to PyTorch --- .../{BaseTorchClassifier.py => BasePyTorchClassifier.py} | 2 +- .../{BaseTorchRegressor.py => BasePyTorchRegressor.py} | 2 +- freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py | 4 ++-- freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) rename freqtrade/freqai/base_models/{BaseTorchClassifier.py => BasePyTorchClassifier.py} (99%) rename freqtrade/freqai/base_models/{BaseTorchRegressor.py => BasePyTorchRegressor.py} (97%) diff --git a/freqtrade/freqai/base_models/BaseTorchClassifier.py b/freqtrade/freqai/base_models/BasePyTorchClassifier.py similarity index 99% rename from freqtrade/freqai/base_models/BaseTorchClassifier.py rename to freqtrade/freqai/base_models/BasePyTorchClassifier.py index 1cfd742db..c08142876 100644 --- a/freqtrade/freqai/base_models/BaseTorchClassifier.py +++ b/freqtrade/freqai/base_models/BasePyTorchClassifier.py @@ -16,7 +16,7 @@ from freqtrade.freqai.data_kitchen import FreqaiDataKitchen logger = logging.getLogger(__name__) -class BaseTorchClassifier(BasePyTorchModel): +class BasePyTorchClassifier(BasePyTorchModel): """ A PyTorch implementation of a classifier. User must implement fit method diff --git a/freqtrade/freqai/base_models/BaseTorchRegressor.py b/freqtrade/freqai/base_models/BasePyTorchRegressor.py similarity index 97% rename from freqtrade/freqai/base_models/BaseTorchRegressor.py rename to freqtrade/freqai/base_models/BasePyTorchRegressor.py index baaf097ee..756853496 100644 --- a/freqtrade/freqai/base_models/BaseTorchRegressor.py +++ b/freqtrade/freqai/base_models/BasePyTorchRegressor.py @@ -13,7 +13,7 @@ from freqtrade.freqai.data_kitchen import FreqaiDataKitchen logger = logging.getLogger(__name__) -class BaseTorchRegressor(BasePyTorchModel): +class BasePyTorchRegressor(BasePyTorchModel): """ A PyTorch implementation of a regressor. User must implement fit method diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 16866859b..20c0b0c65 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -2,13 +2,13 @@ from typing import Any, Dict import torch -from freqtrade.freqai.base_models.BaseTorchClassifier import BaseTorchClassifier +from freqtrade.freqai.base_models.BasePyTorchClassifier import BasePyTorchClassifier from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer -class PyTorchMLPClassifier(BaseTorchClassifier): +class PyTorchMLPClassifier(BasePyTorchClassifier): """ This class implements the fit method of IFreqaiModel. in the fit method we initialize the model and trainer objects. diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 861d90a21..df149ffbf 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -2,13 +2,13 @@ from typing import Any, Dict import torch -from freqtrade.freqai.base_models.BaseTorchRegressor import BaseTorchRegressor +from freqtrade.freqai.base_models.BasePyTorchRegressor import BasePyTorchRegressor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer -class PyTorchMLPRegressor(BaseTorchRegressor): +class PyTorchMLPRegressor(BasePyTorchRegressor): """ This class implements the fit method of IFreqaiModel. in the fit method we initialize the model and trainer objects. From 36a005754a955747cb1bf4a5f2819a3c3dfa7abe Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 22 Mar 2023 18:15:57 +0200 Subject: [PATCH 090/208] add pytorch documentation --- docs/freqai-configuration.md | 88 ++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 886dc2338..f1cf37923 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -236,3 +236,91 @@ If you want to predict multiple targets you must specify all labels in the same df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'down') df['&s-up_or_down'] = np.where( df["close"].shift(-100) == df["close"], 'same', df['&s-up_or_down']) ``` + +## PyTorch Models + +### Quick start + +The easiest way to quickly run a pytorch model is with the following command (for regression task): + +```bash +freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel PyTorchMLPRegressor --strategy-path freqtrade/templates +``` + +### Structure + +#### Model +You can use any pytorch model. Here is an example of logistic regression model implementation using pytorch (should be used with nn.BCELoss criterion) for classification tasks. + +```python +import torch.nn as nn +import torch + +class LogisticRegression(nn.Module): + def __init__(self, input_size: int): + super().__init__() + # Define your layers + self.linear = nn.Linear(input_size, 1) + self.activation = nn.Sigmoid() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + # Define the forward pass + out = self.linear(x) + out = self.activation(out) + return out +``` + + +#### Trainer +The `PyTorchModelTrainer` performs the idiomatic pytorch train loop: +Define our model, loss function, and optimizer, and then move them to the appropriate device (GPU or CPU). Inside the loop, we iterate through the batches in the dataloader, move the data to the device, compute the prediction and loss, backpropagate, and update the model parameters using the optimizer. + +In addition, the trainer is responsible for the following: + - saving and loading the model + - converting the data from `pandas.DataFrame` to `torch.Tensor`. + +#### Integration with Freqai module +Like all freqai models, PyTorch models inherit `IFreqaiModel`. `IFreqaiModel` declares three abstract methods: `train`, `fit`, and `predict`. we implement these methods in three levels of hierarchy. +From top to bottom: +1. `BasePyTorchModel` - all `BasePyTorch*` inherit it. Implements the `train` method responsible for general data preparation (e.g., data normalization) and calling the `fit` method. Sets `device _type` attribute used by children classes. Sets `model_type` attribute used by the parent class. +2. `BasePyTorch*` - Here, the `*` represents a group of algorithms, such as classifiers or regressors. the `predict` method is responsible for data preprocessing, predicting, and postprocessing if needed. + +3. PyTorch*Classifier / PyTorch*Regressor - implements the `fit` method, responsible for the main train flaw, where we initialize the trainer and model objects. + +#### Full example +Building a PyTorch regressor using MLP (multilayer perceptron) model, MSELoss criterion, and AdamW optimizer. + +```python +class PyTorchMLPRegressor(BasePyTorchRegressor): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + n_features = data_dictionary["train_features"].shape[-1] + model = PyTorchMLPModel( + input_dim=n_features, + output_dim=1, + **self.model_kwargs + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.MSELoss() + init_model = self.get_init_model(dk.pair) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + device=self.device, + init_model=init_model, + target_tensor_type=torch.float, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary) + return trainer +``` + +Here we create `PyTorchMLPRegressor` that implements the `fit` method. The `fit` method specifies the training building blocks: model, optimizer, criterion, and trainer. We inherit both `BasePyTorchRegressor` and `BasePyTorchModel` (a parent of `BasePyTorchRegressor`). The former implements the `predict` method that suits our regression task. The latter implements the `train` method. \ No newline at end of file From fc8625c5c5fdde64f7b5737ac1d521bf3a8db7f8 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 23 Mar 2023 12:13:27 +0200 Subject: [PATCH 091/208] add pytorch classes uml diagram --- docs/assets/freqai_pytorch-diagram.png | Bin 0 -> 18611 bytes docs/freqai-configuration.md | 9 ++++++--- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 docs/assets/freqai_pytorch-diagram.png diff --git a/docs/assets/freqai_pytorch-diagram.png b/docs/assets/freqai_pytorch-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..f48ebae2570b76ad7cea67c4084b350db3491894 GIT binary patch literal 18611 zcmch<2V7M9k}kRs1tV+`1x3KEARaYdbHDN_jt;TFs9uOUsNffu<~CnHp1$%rMME%Tm4iu%64RYRJ>2;G@E<>iCuCCOP? za>WnZxM)d`((_hJL4( zQv2V(f$sxaTH5UF>~M1(_}Z1B6sPDHS(u;C&dbB|;r;uob1vuAD>byWb1Eu?e*I3r z?1@Qg^)sdV!+OGSl5CO^za$D$C; z&URl%R8%xOCx_*}```_;%1K@3#>Bj*tA#1UiO4MtSTSFsDD$<-`pzvkZEfvcd36<) z5SaM(NcKQ)Z~C!Q7n+J#H8nIW_qNxF`xZpHb?)6u2@WPJ#b8>BSbKYV>Kt;;s3k@i z3$$v%qQJ`teW10ab1REBF@z8O8-VGMfIt!r)TBJ z;GmV?w+FCT!WRMtQ&gMs3ku5Me*JlR779he!om|tA$P92-k>ZpExccA9?#Xq%yF3v z7e0TV^`T7aJw3Iji90Jl9}O%nE@I%axw*NWw`UoqO)RNuD=J#a-bb2lyb;t9wZ*N^ zQyETe^>!C{h39ctjs@Pix+gZVE8Oi(=o=V#=URCL5#e>@Ip%x0NmFe$ahkz{jOnL> ze&5#ng!zC{Behv=Lk5avB{E%IU0b>J>9)9u?uyFF9k%G3(`K;wS+lW9YP#};q^Zn%|o#D-6_nd9~%N3L6vUM4v}SXD51dZnr6DS!7n3aVA&;+gmo0fzXYmcj%h&R4sy zJk$$^H6HJdk3K6n=aiTpP;cU@q!i@fjxFl6OTIACo0v5_9~r$lY$;V_p#^UcTfnX71rvs)CiNk6HZCy zcRomZLOVcNbWOv>)pd4rBBbA+De(gYAFrRMjAqJ$D;Fyi!dE+_$gLKK%WI|tTl;!@ zt-inGd~@gONxyGHm2L%TFSN7XcUo3eWoO?^@ZKhze0e*&prD`J``3#|PAWZg8U~o1 z15)*Qe{>dn55K?jK5%JQnK#Xvjmh;2FSS4oQgnMJb@}(zu-EpVe!kkwt0Al_AQIk( zf&H1Hk+a6}@+VoX#u_aPYAuE27pi8CBadHE(HS{Q*kmR(thi6&Kh*$>3#HzE!zA3^t)^# zL`kU%9!mHFG4A+ii^C|95+UjK4S^Ruem65I`4J_5-n?`S(XqJ>$I%t3v7ONk=XTMJ znZM2{cl71yX=-Ub6k4%~+Q!xH>FsUz*V6r>N&oihOn00^O;&`m zP_v>jJev*<5hOGigW;?G-NG`vu<+JgCp_)vH$MfTO*@-?VFf zpU*R>)FLIj9BQruM<05>j}N}B{`8}_?)h+UsMIDJ*2Yvw1Fj)LuQ_f#*OzB)W#!M5 zcIpCaD1)Z9_8b_prjAa``qEgz(2(x8up@}75Sn2epV{A8(l#FIK?w=wtm8S$&sV@NU&nhgB$xpJu{M~2=@Bb>*kk9EXt%xiDj z=*iEQ=nnDP4&16Y;pFAxi#>i0vJ^B3NgyF2zhAE}j^xxH?9}#8goxAmG>7TkpzI1~ zRA7)h*k3weH^d=`gf*Ibq%fD(Y4l^niS*SgSmCvGoP*7m z=f^5$-Upn#coRVw{aDb){i-{sV!1F>Dm4Ah^oY-Nm<(O#OlRsEZfu_~B#a1A-9}gH zS7%>ehUZeXcG7lFX4keptc_N_ewMbOJEIDG=$62v9d-l}!-}jZ(t6CKT=a5<&57kS zl@7jk1bpl#Y8YZ%O0rIC?vT$3|99qE|K@V=hyP#6+W(y!F!R$QzFlRejbx>zr7>j0 zw@g-NI=6cZyv>D9e|~*VSwIq>;Fd_Zg+vOpLb~Am2O_Vht{(BQEfzk2NIX+Y5;JIP zYs>hCs1C)JtDU@JA<%dXk!pl|SJ4kK4*e1H-a}SU2jevmunqQu)}3OC(GT%s2%=Ge!~|2c>uOx}IgzNInj zn2tq4dP|Eur1EA4%6x?)yT85-VeH}>f@m_?^e{&ajl8_PYPT8k`!ROlp!gCpdr3@) z9S&x{>?6LvIUyJA(hm87pj_Iikul^775E#qdK35RKU3@r>6<2b7CSLWI1b|QH41<3TW(vV&%h!j_tKM(p;Ro zSoZt(w_*3K?d%|8XJ=*Iv$3)H@jITOPX|*`p=)4Zu-7t6Al#FZk`kdSU=fov;TuGA z(PnOo#V2U)@AW5nS3cHa4$?p<2@90EtW*j<4ZAoFeWpz<7CkrN+PK6z5zINrRX#%{ zImBpyt@#mL-hr!^`x?b=aEYV8h?VXBM`MYYD09LVK8akT>{_tE?M}-cH@)+9$Wfc~7VkaM6%^Jhs{vS#CmFsC! zh50N-B!@~fG8%%VQ(+@q-Q3D({qGdns}#68i*=8V(uI8S7am-9?YjqK=15e%LAgYX z{>FS#c{b zN4898(^%b(Pz2N$*|%$Ok|a!*qzj8N#XPS0g@13v5Wu8^et6u7fkLjb)}qpYLWShh zIX$NGoUE)u#`*{&9A*KF#S)z;(WX6jY)~s}J*%$$)$Not^XqZb!Sz|0>IE)yedY#! zPuI$lVaSum<(Pfvr>-$8P1Y2JZCc=(3#5-K$w&Bu3vskAPC+&D?`oe|dmZJfwY4?k z@Ry1>ZcSFnO!w0C2t?q70{iw$qd~)kaAgD2!U10vy#3Y?yWz^!FxlD&M#$zdtDPrK z;}(5h@>$Z3ELSkv=VS+4$W~+3c;k;;+kIGk?Cqz*e}pVmdXqEslR5VSQ@kH@*yqBR>ilGLfR~;ogZie>bDK{ z+T3WlM5T{P{@WNr<+678N}XX0?u#5@hOO!9hDC?lWv1Kj-10a)K*L-x-ePz7glfh@ z%*}@6Zi=LkwQM7FEj&F-6}Y_T{37AQ){1+^8RXkCCn>|IusQ9NpkdAl-b)_&6> zJW{Bg(AVFOgH_$j-=9t;E1z}9f?TL6?D}ZUsbq$pzCQVItvuayh#_+H)%VndHoH%S zmsoXY#7FX@br$_ziB!>(-4=!OPVAHK(Mpm=xg3Qth#{Cnd3bp6xc@d|(4B-8TwJ$>M@=>`I3S5Da*%|7%0iIpiD`tmu@7JKTUA|sLNBq{ zmgmxfn{cXA@7MC*KiS*&v~Z6E;<=_}8MuC2kdIgx*VNwF9?`a~GI8shsQO&*K9{-k z#xUFUE!n8nY~8HL1a(?|)+g~L5h{Xat(lAK=+LY;U2#oK%@|w8;Cg_W`A%s-Kjv3< zjvf6xIrh}6VQuX1a)V)xQMvUC2K;z)u|*4oY6csYOvPHex7JDWPsz9ehd`a5wL8Zf z`*`tFycf>v7+;*wfb0IYo+=!3q1Pa3zv4-_o^EIqN^uZR<^JhzYUd`^>>~k@D;FKd zbnolw3IAY=#72KbXAz)l;_lxngR`shBieCs1D4!Il|vA z^s^IIui=@btF~puFbYKiYL1C^O4pk__Ar{+UM*I{nM(2ZpRZEYeySRMq#wA)b^~%n z!7j5wjNK`usH3MRO{e%_7Bx9jl)0SRy@rPr3(rk@A!)$bS1Kt)v2xO$RC{E9&EDRg z@A3&>ivI81km4p=^6>JeKV3YB-1-DYOI=b?Q6X~R^C8th)fQ|93=r~vh|N4Mvl`uh z5+Nr)3_Bx8D91!Y(A>tsaJEN>u`Lzeby9;w%7FA5-mU~Px>$aP#GjT3@={vuq;bbp zqGdj6ocXOjs~a)=4s^{okkb#)DhXcy13+Vo5-DQ*%hKAKi}uKGv#}blp-+yfFh>SC zzsrwbx3un%|M|_2ydSE|E{7o~o_L^gBAOG1eBXJ#uyLBh*vt-(O+XKCDEiPMiI|jZ z<{P^ZKBLP2KCW zuWVlG4;7mFiETBbnC#~(5@_mrnAILd?#}oeZ1^bo`HI8<1P3MV=_66#oP;Y-cH*^b zx8KIb#g%%jS%Y_~Uy}~(*x%dn5?41g%um{zx2Tv5S2#(@ShO6Gw0q}ZGvweyO^q1i zPQ@+TfiFzYUXj!1L%1Q>812NeSFft&zd3?Dw?>O(vwF;e_lenjgG!Ne8m&dEXt@Ky z;?>o=*At(NpJ+=Ec9T%k)zZ3-!|;LucLY8%le@j*Sf zw!S_KC1@|2vq5o5p@v8_Jb`-Y3QIxleg))4&R?~HIxo@6Pt>MOqTaxSiKuT(em@ZgDwDYeE?GD~6ap-5x$OPJsc+BNh zEMRcs?V(Z8(EzzB6jZOrdM^hUS$_TN#P2>cl+s^lQ z0cg+!r~)gxq0h_98y0w)i(GUy6u5+QKAmAZH#eKYuDNt8@e{~>HeaCS^V{fXo1qe` zOXA*D_LU2KV3FO%lZdZLJ&+3$Y8+L+o?l?oP2;ucn|Ws%d=HB9>Gq_8)Lq$NEuZP(9tjt6YdDvpu-tIKH2$5*7i6H*fnbeTk zP8s^%#gR&0yCEJxa%RT^IM^TcAih@*pX#%-zR=zBmc?Z{#$xvCDUKdJhwAjH)@YVX zL9sne`G>nW26xhq}VC?29nn zBnP!f$8fn5dJ{m*m{o7zlnV+9Lg(qdzmgOYO7>@}Oh_MhyGUxcinzhSGX25XO(%T3 zQ*acUUWP%XYgR^vbi4RL6;@#4wAZ3j61s?6Iu{XYlTU!oNQJIMu%o66-kk}_#N2F7 zK_5Jh7oucI|Hf9kkKw^~!9lhHM>P+5=lg*@f{IU9dbDA7sZIavxunfILrF_>bKOuT zY3k|apcVl;*~eeAe8#|QtrraFDcw~Pgfi?Q9AwnDA=N1{sXrof6CjaMam?e1d( zC@)b^#!N>`dhRL2(enZIJZ_7_mt_LfiU2=A@h@!y7fr3XK|s6EEX$A1L)M{o50jys zkZn^?t!1?TJDPhj74dx!rx7Cc1?FAyWZ8txs74=rELPMLV_&r_4X``soJ%brLP85A z-H-oZls90#fL6;+;UbG;wMOC)vNd1|_(V@-6y{l7aPZL^+rqkq*VvvyRKp?THU9Y6 z-e3RIqm;gUgPaHzF`bp^wr+Hrpk{wZMt*#LqxPUWidE-*Bb}!rz4vAcz`iLcs%DYk z5=H1hkilz!+J1T1o+zqY?NNGxUH`+_-lAK@kLRbHM_l>|Ei45Z04))I`35eGR_j3+ zL-GE#xUpSnbU$yhu&k99cqzgBKM;N)NDPVre=z~TPZwhw0|dh=YIEEc*dDgB;rCz+ zBhHo*Si6Cbk`P2Rq@-R*htTB}`0UM9KsE0SXk8rH86=brHs*%Qzjo<%CpT(}{mn-4 zSv1IA3lVt_u&w26PgZtr?j#n#P}*H(W+d{x*Uq}tRLfhTmG9gxKVMSKLeARtd#V+1 zb{Bwo!b44(1l9*kUFP!2PJh8;(3U!m(-s zm;2xp)(oN(9ddqx_`ZN0Prm>6vjlbiM9)P*r9=@OZ6mMJX>3Byy;SKKGV&YDG<>$I zc6NEFX#$XIvpzobvOkLSAW z$dienSOLfPgbrWH%z^?H;7PS%Ac-Ec%t1hS7#(c)m$T6!CqsbjReW4S%>l8^ra!;8 znJFn3s#`*KPJS2E6^eeOr%ymE1Q;rT&VBOi9zZ~y0H!pIoIq3sb{S41?><_z)AjcC z%@!x^<+7JgQdGmSX>DuU1N0lZtKp$XjydlxH_kyI?pa^JX+p$>6`l=OVDHQn-?JJ4 z>~y=;Xx~KW4np07hMi+2E^}HiBi4QO^MAM4udQ0XS0}2Lw}FgNMUM(_o;dpoxdK>VyY*x#Ay*~|Wi7NFFs`+9;CR(X0yoM7!&e*Io#sDJ=I7ES#}os0CVvqn4s{`dygAQOV%d2W#h7FH>|*n4_dODP z_O~*mh(n3dD1`#W^#N2ikYmOQxn!fZI`vi?9Oye3nW{Bd$QA8)iw^W-kaOveZTFRJ zx}PxqV{T!gx7~Z4n@zVw7Mv!aR&)nIX$+ctfyqc@T>^$X>O0u~ zDsUY2MkZPSZvjMS@N~HzhUIG_0~-)CLm+g**jGOjlUu^kTCw}Pb<02sLC+Pf0>eBR zI(LSBR(Cks(TvU-(A$h}aOoYmlrT_a9&mxfFc(hWoZMVRl)DMyAN}BejwzPxj6~Sr z#|JO0ZEQ+`b_}BCybaoeE|-3TOu2~b=*qo00V|i*)!(1FHrH?S^Xp69N>{tx^1___ z3VG9pKu%gZ->dyVlOOE1AM^nJi3Snyl&qp6HKVN-Z4E<1dm$0zR-XGi!f_<|{=#6f z3=|~W^A_Sc=pnyUvy*>`hNcuqNHnKoNJEZX6n4$EuU^xthRWIl4j7loFd1vefrXaq z&Y`)JRbw!1hSX6c6*bHg82gcIAld7U?sGX!D~>#FWfR(4?N;(5F%EsQ(#8*jN+!&E z4oz{P!iRX>>_k9bU9}%OhBThys7k&YaKQ>98_FlZ53{yb@N*whgSlug`AS+taRS`T zJ=90hSQ0OA4>c>GTC%{zx=?#K4hifl48&7_>+Wrg`S|QA#q=9`=0U2K3c)3gjQsN- z<5koI@R1I*mH|sWjB68tfQyqLTd+UImT&%7(Gz*~31A9fXsluFw?~$|8=JY17qkE| zrIhaM&W-WHwPc>7U$?H0J;swoZw`DP}tNO?NN(lWFzk^5Uo2ReY8qHBr9s`A?~ zfM0;4bVCAK3VtC9se7!5dr^MPwlx$R+Nsh(T(opZT8T|Rd!BBoJY>ZmT^EMZv(z(Z zhinRZqic7~4FiuR_bf-;LP!c=sAxvUl=QSH|I_%%pv!`4;*j*O%^4l&0&fS~E8hUw zCe?vX40yd<7i(Yw_*u5s7KoSK|)I4gYJSMIKM1XSY`F{S=rSn#%#B zD?&q_OzF78?)xR>Z^5&sK6t_!*ePk%R~6>xsV?9HHy+zqgCq5P2dYI%88qkaJg zgyz3Eu&PV@F56ow{YTI8D;I?f+^60`T;PUvq3n5ehNk4v&)a~fTY}TY z3OZ#Rh7=&cP~SC^7iYFREOim7M6Y!nfYfu!%CxBrpQNx?&U1PFj!5F6y+Gn0!)*~u zCP^`InQLXSa>*K$Cc0(z36Ln&&V1)???Eet-@hMsrx4vryWhY|o}Lu>yFn0<5Tk7U z)g(ZQG`%#r*e}9E{-)a;Vr6~&7BM0(`wxC5(3=_$*~iTWN4B%#CxCGNA1U)8P@+$2 zI6atFlA0<7Hdn-|0AHa@i6De>0Y#q<767suv+8^k>*(28YjD*a_K4OBay~-InT7kq z17b~gIBDC9>jfJ3mtH#B|7vPnXI|yoz$Kjv5e5wq^TytvjqHx$^9>CRC1S<)BSIi! z2$F^pePMgsLzwd9)HM+hLsV8);;oZ4%A^xC?0Zod*b!`{=z%Hve{gTub z_kvo`dQ<|@JK=)A8Kru|tiP{ssPI1PoD1VltKSt|V*wUQF;u{^LV+{x85p>)(MZ`K zh`t>Houq8y6?#TT?PacpZQ?;8fU0vEO*r+`zHB6z?G*PrHu{&TzOmC^>CouuJQmh$ zyx`n*+sewS+0(szfPjjiz8WX!3)w{kxH^+OuUiu!orD52w=c<#6%-g^zQ>fhs>au| zQ@NWa6+#y#H1YRYFBCXgSXhjX(Nxw+hVm3%tRM`5mdtObm-9kE zeaKy`$D0RZ%&#Vt3xjOvUOiOPZ6X^2zj!G!p2Z= zf7n9xMgFxWj}@=<8#C+60c@5DeibG(RTuGOCh;uQpH4kr(ZM4e!q@m z)HL9BC;LpRqN({7K(W4pHnq~6l|c}PxVX5i*+e)Gb+fR$8N~BFYMilculqEzEZq9Z zR1u@dzI`{;RW7=iBBd@oZB+ zP8T`7(heU1lX7!)ZD?l)mL4p+-ofUPefy&^5`282d43l#?HrC_@%mZ=(~UvgWcI#cH#4Gfr|t2E~HldAeGvz-iHq@>lqaR zwFT_XM11`zzi-Q*BJ3P2XtBjNl|@oiZ;X=0n->wN3>N?iZDzpP|NbO;V+>c%a$!SL zYQt4_a9!iQ;n1!x4%hQ;%7-5Btiy^Kt2DLmheOhvb( zu7ldEp#;va4!De2x4{y}Kk8kjV;qCwy_jlFjeoT+fG<($uy|NK8`j1r$Jg%Qvo5^& zY0GZOwh?1O(=YO9$9|k=<4ZfHdMPPd%tJtM@EOkG<2p-wxr~@jE;jnkhUhBxOQR0^ zzp21Rz6`Yqh6<9lT`?~we}v8Bket2rmk}-__K*g$2z4aW2Zp10T&@pg05_@p?{qCL z4cbT7amXza)m>Z#!H6v(mFkZHj2-O7wis1pt^>*6X&|Ap8|FiP@n62rwaR38eN7mC z=n?n}hw)x1vYJRf^h?ut^u3ZD;^`%29)GH$eUmK-CdS*&f=S+y?AVWM9?-I#h$RhI ztF^+69N0>aC)qLiJQ18RPEh2z6hwb#`2Qtoi~AxpQWXCLPHfWo`525v_WkF|E7l4Y zS6s;^VHV!zK$3vz8)72nb8Txd0sSKQg{Q$-tg@b3ZQ1pT_PJFiJfaI+f zYXKE^&*zTV6PWS7n;PH65diyRe=E?bd?q?r5#v8r z<-D$5U=_1E29wlZkx-lEZ*%YflF#j9eqLTFXcTu@EkhGHsS+?93nsXjUS4{0yV0uh zu`i$J2KF6I^wP7{DN-11%s$9mZAq!#=8Ta2!h8$7ocwg@GFCK1B6`Y1)jqIcLL z=A`b!P%Hm3`E>P+mAegsI}5wQO|G(~_O*6#&93depY3q<{7<;C$5=RyA4cTzE;{`1 z+VQJ8F){WLwvCz2nd#hPDroSNmEX%K&t&q;V#ohVscNPAvL9ljn#n4-%eP z!5Yqee;BvfcB)I}g<@;kL5AADnT6Lc#Wy_%g3lo6FZp7lOBIt!<<9Gk*CzapqvFcI zF-E^|89$#07T#VVm_)ADe+AY!;YFq=;%NWY(Z2Qj-CEfTDn+hxnfaK7z2^Z>9ipAd zeqeu4OuFnh_tBW4tn@I)@@whTnio$ytePphHPG>g|F(6`XgpqPH^=|akw0ri$L*dM z;)oqHzKD;fGGv?>j-qi+AlOD|tUSNG60QWpC((rA`=vxj3MI&F&nVt0qQ9*ikKcE~ z<_kpc9g0}T$u}z6Zvywf8HH!136kC!QTq`e_L%I+;_qRH9f(EDronfv64%7X+ENu% zRP-;2(csTl7SrL4XPy$d(U+)~<~aTF$i#)d0|>hF8~72@m^$k4-a+@cgdM@RaC~}` zPTKC=Eh^d@s<+Vti64f1owR^{1&eoetVOOh>21eT@qS7wUk=aBi|0CIbZ#!YQ{er+ zRJW{VCmGo;TD+->+OU=?a9`YyY(qoVc3W#k(2JXxFL8$v3yoUre8!?!ixE&4ieSvv znjXe6#6{=kYBbUoL|;#zt&O>Xacuk6AunIe=RBU&KbUICb4k48uWvtzNUb&)cY$pp za(Lk+WX3>ovEwI2D+p)ZrQTI4;&a1yuFMZ>;puuJ>f@h;vPHQ%_VX6*niUE|Mo%T) zgx-O}RjxN5`|OM0O2QLY0ZoIwC%ud_JTj~o0+J*ns^~wqXBaWC;0g@72r}5YiRyn+ zSR8WyiNcD$QdmwZlgYNX?K*BVGvcFc)VfOjv&>H2Zo_s+;58imjDJgZn4}@Y5XP3f zxVhoj*x2?O-q+cM&aVvGMwqQUucJ@1dttkz*l-xCd34}JzWE@^94(FjHQ=F@7srN7 zMLk)WzWg7FF!@fjN|g@2oV7gBNd8XDalB5llAizXf^K#3hVri4mC5(PUkvK`^4%@z z9twd(l0->K33OYp2)Euy17HRc(H^1`Sqwom{K^0Xk-R^Gn7wf%s}D@$A^%^pXy=1^ zbF_2v^OYfAoa6UujbekK-!(s2eCF;elh4b;G%(z60)A?eYO$nHbgk?jq}boW1UMzD ztoe;k>L1I^%j*ST)LbBTya>u@TJO!D+ds*DsFxZ+Jc1HTkTzNfiBR)Vhv3U^QR7}Z z=9h3{Q#7Dq!1&h9@txA3OMweq)N?$M-{bg0jcd{)-mz4}>U_Cn+c1 zsReQp)z1Kal!X=Bv88nzcFd3>dLzvo_-B;Cc(gieD-eE&@gFy%eug=cfu5MW1a!gj;f5Xr#&4YnU|;~60Z|Y21`5{#-)ai?A%fO~7k5BDg;snh zw-b(*w1+t6<@3OdF0Xd0v=*@rV)9UBk+yam*$D{=#sfuWgaBYkM~p>c7S#8(V|m^O zdt(QAfUox$n}K*2FoGSPhu^6eMk@Km+KO0RoSkQeo!WV+FZv>=5-#!+imajVAn=1b zjAJeG_XB7OPm4f3^hm`E{4F4X=Z5DX1 zu}IwmeWL*Q+$BzvJGE9QV9jSYlm_6(#WyD_{7AnyBNHp^X5Y1yv{}alGt56C zeH0!9({h|>Sn~|I{WqN`qnsbD>$}k>6KB9zuo|zCT_~JYvkiUr0J+zKwuUqGV*2Pu zQ2YvImo|U>3{NW?^Nx!KK{H7y@FI1M6;P7uM5RlS2{P={)S`UP8UA5ot~&@bAa{&2 z0A1&xAOKpPgBBKm`g}vLHBFHAs=$pZTUy1pPmIIkF9lK}zPq5*>V19o8p^VmhKOo8 z;jz7g7D;(P+hr|3XIrgvoLB`6ruB~GBdDFB)Q!jz9{J!^NTI8bhVq|(g> z6hswkeR`VkBOp8pZ?z3MX+iPnz5=~B)(880vjBL^_U5=WT(qzRu&x4FhSG;^mr%F@ zV`l>*y`fs4TEZiuCxF_?e~0m8Xy#}UBGr{UTRCcTQDza8z3O^;alr!P$GxVxlRD7~ z9mO3%Jy$h<9Jyr)WvCKLm@*Ke^z1udQW<_YaqjxeLfKd!YA`^VfuUFqe{%FdK`M21 zQvnmi5Z(LVKGq;qZ4Qb|02!h6)DWJPp&0WXW@7~~Lj^1*gB(ZoYIqhXYMXNU z`m|t&yOo8N#5cc?GsvC0uBZw^VG#H;SpA-C_5c!c4G?=>R2m3?nKhRg0;$k5ibBr~ zdjrpf((UCCpInru0k~(D-=`~0ZX4@XbM9aRC5Dt@dE|U{C+OV=&mzXED3FWN*HFCE zK5qImoa}l!pcxNp3mrg}h%#!cFFBq{uHf7G&$Ih7pbuFF6j~9)p(vBeV>}Af_Uhh} zj}iUBX5&ORK;x@FUXaTRZ9cpWTH6TNy*XH{4$vv*abpFpiFtvLf4j}WI~9f1#(vZ9 z3^bMirbFwqyX*nT(Mi}>dej;|+UcRlC1>O13BiMUDkGGc0seH@yPE?5a0AytbCheA z#Mkc6)vlJednxmeICuNH!%F?CQYx#e(ubfQ!B7n_vwO%{iCgF`A9>rb#Q-9Jk_g}n z1HSGkw+HP|zt3Mn5NXf@)plfS=77oa85G%_ZE>5_9Ut3*yLSjL zRiU0hb>k{Bh#IjzYOLb>PlD*hN1bmV2<0(6W>kFlC;3BSqCY^eraEr;bouVy$k<*2 zsC?VlNb(28cekEdfJuNJEb=3)Yy!}_m&BfaBB;V6Z-Vki zo)m`i6|$mFE>s9Y4i7lIQx7^FDEc9;AU_@e!#`B*S+)<9aU9t(iIeCs0Kkr5P=ccR z5%d#K(&NkI%XLpuio?+F0NiYaS~T!t_Ck~h@&{jfOa@(vCAc{{r%iNDR_L72*u4rm{5ja#TeQ$ycQ+u7@&rpg5qC^C& z$tL>5?^8nRq8G=!oF13!f%}yFnQM6SxYLLF7Ah zfxXrTF9|+QJ6vGvD05Ugy#0-S-xwA`40X;GVC^5zL2M*iCNWIjghP^X62;0Le0KB< zAH&fh0aIHB9#mddM#c~AFaS$MjsW(9 z`|K%%zFll57i|vN0|VfrJ$Z{E{?XSH?q1z4>gwvFeP@zurN~7!fWbGbC4TeydkiipC5x}5zbhU0D<0T1>@aX^c!OjR-}1*n^~@nI6rcm^)&K1^cIjI z&m+4B24>tFs1e^U4NK!pXj2Qk{*Rh5|MzYns3Z@zH&;DPZBxzJNAm4{oN!?2AoEmVU(P(x=$pUMBpYC5Nby0HfDLvU z{4Jy$8=OTiENN<*y1*%z4uZ!{JXbFiv>U$}%nhVdaOsiexY3t0=hCkxV7}iK52-gH z?mGuPgt81K3Pt=5qn8DuAnlD@Mm6v*dZbnnP`4SeUnXNRzzUMc9`;naFS^bxF0Qk2 zSkid1Q3ceGw?Z1a9rfZxhDwsXN|Kmpfw^oE4Gm5A_G!~R{)V9+)HY6jg?z&4B7H)C z$7hw`xyyTXIx6fKAIzJd0)z+)zA08_HZxOD)#6fAKQNHAn9Tf@el-qrQzPld`ImGz zBJvsqKkb}h@Si(gG`hHGKS7*ctPkx4Y|S9qCZ3i7o#d|BT&t^|dugFj+_8}hQU-=dxvyxG&)nF6^7B!MWPI5mA`3K|z-SM$fr*Xk60{i6$Z!X-{G z^qmq$A-jU4I&*|JHEnYnvbXVta68f6AsmPD2A;K54d`qJ#)Wf)TqYf1!3Dj)wa0E@ z3`~SQ@-jgYTSLc0vQd)kNMfbn#wxj!|4nCVR;B;Ub21KQiE+-P0&MiquCfXhDQ$VXa#!U zqIl&WAU6dCeXWb}NQDcrJ&O}oOd$G4f_da*;0q&n?9?paJ+)`9EZeF<7cU5icZ+JA zTA%-WKv(v=av;X*mu*b(A^@%y2IRHt>jB)9CkMl18TOYR30yuXGt%?S%fw(XMpa-= zV>-redl73i;)`nnE^gRZkR?mZ!4cxfuC~F`pbPJz)lAO5>kc%yhq@NgizUlLFz*)G@4>10>RscJ8E(MF1ryy_M=l4xi0$gDJ{&ut7? z{Z)h7B7lgA>7A?k=>6|(dlfp>*a}+c+f^P6iq)=NZ*dkLa$Ve38?Sl0nJbqqlolIp zt5eITjEU#AWNI<5IgE<&s1c>}fBY_1r(w7ovPzc(1<5lNDS=-c|78%d0?pQGBNPra3RP3wGagj}q+QUBzL{qolUvr|#WTp`=k$>CQV_6kxnmct zYJYbTv||=){mWF+i7Bxxlp*5qrLa3jV%Z+qhmp$S4 z^BQ}c!^oCxwBlT>vQF0GNPGXfi))Qp`-Nk6HWs4(WtaJ{7i#(gdrwvujV}myn86Hk(H=k{vyGaq(!6s4T($^WU+(o-;(O1lS#r%Lqk^IWpCp<*Y!Yz(!j|xfuW8HJm98&NaJc+wlt70~7}fbraf#&=bQ6 zEaT-H3B@RcpwoLz*3&Z&w-vv0mlznvg)eP_$3KyGVDoNxw=+_DB%h|RVBfYcdNOQr z0F2;B#r28H5(#=fTsjUJxK4~ye>B`z#3}rVev@;ut%C#k(+dKzL({*wN}XVaX$?M?@R7AND0AK##SqoS~TR(B&N4oKRu-|^mM(OhUV$aO%!W63MZW=~B9acU(ab8#87rn_FDTq!;a`F9%j_ zD^x5@R55(nt5V43Q`C15!gbnIH*be)#~7|TML9K2&95wSuzS1w3h1=Dwh?gQc}by> z`yw7LOzPjC{D$*zX_PYQ0!z_1F`m&T)wxmEZEtwT@7krH z1{|x7Ex1`sU|4gDyi2^I2+2`GYfO`k+U$80P`((80?OTI$^1w~o-Wi}HaP5zuuz_m zz8Xdp6n)=tyaDL45c zMsu;GAah(q>TJng(O|Hl7mtq7$)cd$@!u84ZgjiI&z6N`T2EYQEhoKhQMtWP;9ubD zb|ARN{*UOmfFXZB5Z-mg>t$io;hXQQsoi3uZW$yumFel}V^640((ax}(7=iJa&{N^ z0KTq7^XV8YLSTWnT-Msbsn<3D*5>LIN8T#GAt1i18(0_<8RD9f7p4LU2&xhFWafo~F5XvmWH$ zEnIHqj`NE-e*AMsUr!I^L08ClZ%0Qm;FB&AfZYKQst!VuXCDwm@>bvb$Vf%Cq2Au9 ztph0ov7wh3X))li0a%0_2_PXW5MyRY-98Czky?=Npp3P{Hh@??&?*8AoiuMwZ+%@J zqcLTZKL-D6HZ!FE9d2f)U%0K8Ieqz;`rglL*j^0CUO<9s@G4dEj~uPME~tv~>Gyy6 zEsH@uorkJE0mRDqCPnvXccbCH4T-7UHnq*YZ0NoCgR))kNvlj{M_*Q_Gy&Rh==>G^ z(d-9RD7Gbv##w~FIv#8{N^R4Y9Msi#KR`{B3++^g*9&rxg2C&*@wd-JGR1ZF^k_5* zpb!LFJu-pLw<;Ps4-+@I;NM8RO%tKXVv&C|}5PJG@NpuJWDy+s4oT EA8mju#{d8T literal 0 HcmV?d00001 diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index f1cf37923..ba2976bec 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -282,10 +282,13 @@ In addition, the trainer is responsible for the following: #### Integration with Freqai module Like all freqai models, PyTorch models inherit `IFreqaiModel`. `IFreqaiModel` declares three abstract methods: `train`, `fit`, and `predict`. we implement these methods in three levels of hierarchy. From top to bottom: -1. `BasePyTorchModel` - all `BasePyTorch*` inherit it. Implements the `train` method responsible for general data preparation (e.g., data normalization) and calling the `fit` method. Sets `device _type` attribute used by children classes. Sets `model_type` attribute used by the parent class. -2. `BasePyTorch*` - Here, the `*` represents a group of algorithms, such as classifiers or regressors. the `predict` method is responsible for data preprocessing, predicting, and postprocessing if needed. -3. PyTorch*Classifier / PyTorch*Regressor - implements the `fit` method, responsible for the main train flaw, where we initialize the trainer and model objects. +![image](assets/freqai_pytorch-diagram.png) + +1. `BasePyTorchModel` - Implements the `train` method. all `BasePyTorch*` inherit it. responsible for general data preparation (e.g., data normalization) and calling the `fit` method. Sets `device _type` attribute used by children classes. Sets `model_type` attribute used by the parent class. +2. `BasePyTorch*` - Implements the `predict` method. Here, the `*` represents a group of algorithms, such as classifiers or regressors. responsible for data preprocessing, predicting, and postprocessing if needed. + +3. `PyTorch*Classifier` / `PyTorch*Regressor` - implements the `fit` method. responsible for the main train flaw, where we initialize the trainer and model objects. #### Full example Building a PyTorch regressor using MLP (multilayer perceptron) model, MSELoss criterion, and AdamW optimizer. From c44b5b1b3aa165bc247348748102d587262df351 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 23 Mar 2023 12:41:20 +0200 Subject: [PATCH 092/208] add pytorch parameters to parameter table docs --- docs/freqai-parameter-table.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 275062a33..122d87459 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -85,6 +85,26 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `net_arch` | Network architecture which is well described in [`stable_baselines3` doc](https://stable-baselines3.readthedocs.io/en/master/guide/custom_policy.html#examples). In summary: `[, dict(vf=[], pi=[])]`. By default this is set to `[128, 128]`, which defines 2 shared hidden layers with 128 units each. | `randomize_starting_position` | Randomize the starting point of each episode to avoid overfitting.
**Datatype:** bool.
Default: `False`. +### PyTorch parameters + +#### general + +| Parameter | Description | +|------------|-------------| +| | **Model training parameters within the freqai.model_training_parameters sub dictionary** +| `learning_rate` | learning rate to be passed to the optimizer.
**Datatype:** float.
Default: `3e-4`. +| `model_kwargs` | paramters to be passed to the model class.
**Datatype:** dict.
Default: `{}`. +| `trainer_kwargs` | paramters to be passed to the trainer class.
**Datatype:** dict.
Default: `{}`. + +#### trainer_kwargs + +| Parameter | Description | +|------------|-------------| +| `max_iters` | The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs.
**Datatype:** int.
Default: `100`. +| `batch_size` | The size of the batches to use during training..
**Datatype:** int.
Default: `64`. +| `max_n_eval_batches` | The maximum number batches to use for evaluation..
**Datatype:** int, optional.
Default: `None`. + + ### Additional parameters | Parameter | Description | From 952e6412137e2e7dfbc79a8624bc7d781cecf722 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 23 Mar 2023 12:43:37 +0200 Subject: [PATCH 093/208] small docs change --- docs/freqai-parameter-table.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 122d87459..2d875fe89 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -87,7 +87,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the ### PyTorch parameters -#### general +#### general: | Parameter | Description | |------------|-------------| @@ -96,7 +96,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `model_kwargs` | paramters to be passed to the model class.
**Datatype:** dict.
Default: `{}`. | `trainer_kwargs` | paramters to be passed to the trainer class.
**Datatype:** dict.
Default: `{}`. -#### trainer_kwargs +#### trainer_kwargs: | Parameter | Description | |------------|-------------| From 45c6ae446f19c7c2216671778f58391472c6dd2e Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 23 Mar 2023 15:04:29 +0200 Subject: [PATCH 094/208] small docs change --- docs/freqai-configuration.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index ba2976bec..63d7c571c 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -283,13 +283,12 @@ In addition, the trainer is responsible for the following: Like all freqai models, PyTorch models inherit `IFreqaiModel`. `IFreqaiModel` declares three abstract methods: `train`, `fit`, and `predict`. we implement these methods in three levels of hierarchy. From top to bottom: -![image](assets/freqai_pytorch-diagram.png) - 1. `BasePyTorchModel` - Implements the `train` method. all `BasePyTorch*` inherit it. responsible for general data preparation (e.g., data normalization) and calling the `fit` method. Sets `device _type` attribute used by children classes. Sets `model_type` attribute used by the parent class. 2. `BasePyTorch*` - Implements the `predict` method. Here, the `*` represents a group of algorithms, such as classifiers or regressors. responsible for data preprocessing, predicting, and postprocessing if needed. - 3. `PyTorch*Classifier` / `PyTorch*Regressor` - implements the `fit` method. responsible for the main train flaw, where we initialize the trainer and model objects. +![image](assets/freqai_pytorch-diagram.png) + #### Full example Building a PyTorch regressor using MLP (multilayer perceptron) model, MSELoss criterion, and AdamW optimizer. From eabd321281a3dd112ed0dbcd325a1b28b9b5625a Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 23 Mar 2023 15:59:57 +0200 Subject: [PATCH 095/208] small docs change --- docs/freqai-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 63d7c571c..92cff06ff 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -283,7 +283,7 @@ In addition, the trainer is responsible for the following: Like all freqai models, PyTorch models inherit `IFreqaiModel`. `IFreqaiModel` declares three abstract methods: `train`, `fit`, and `predict`. we implement these methods in three levels of hierarchy. From top to bottom: -1. `BasePyTorchModel` - Implements the `train` method. all `BasePyTorch*` inherit it. responsible for general data preparation (e.g., data normalization) and calling the `fit` method. Sets `device _type` attribute used by children classes. Sets `model_type` attribute used by the parent class. +1. `BasePyTorchModel` - Implements the `train` method. all `BasePyTorch*` inherit it. responsible for general data preparation (e.g., data normalization) and calling the `fit` method. Sets `device` attribute used by children classes. Sets `model_type` attribute used by the parent class. 2. `BasePyTorch*` - Implements the `predict` method. Here, the `*` represents a group of algorithms, such as classifiers or regressors. responsible for data preprocessing, predicting, and postprocessing if needed. 3. `PyTorch*Classifier` / `PyTorch*Regressor` - implements the `fit` method. responsible for the main train flaw, where we initialize the trainer and model objects. From 8903ba5d89eb434ab506b55ff703346a5b003d43 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Fri, 24 Mar 2023 20:35:55 +0300 Subject: [PATCH 096/208] fix enf of file --- docs/freqai-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 92cff06ff..7b08dd67c 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -325,4 +325,4 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): return trainer ``` -Here we create `PyTorchMLPRegressor` that implements the `fit` method. The `fit` method specifies the training building blocks: model, optimizer, criterion, and trainer. We inherit both `BasePyTorchRegressor` and `BasePyTorchModel` (a parent of `BasePyTorchRegressor`). The former implements the `predict` method that suits our regression task. The latter implements the `train` method. \ No newline at end of file +Here we create `PyTorchMLPRegressor` that implements the `fit` method. The `fit` method specifies the training building blocks: model, optimizer, criterion, and trainer. We inherit both `BasePyTorchRegressor` and `BasePyTorchModel` (a parent of `BasePyTorchRegressor`). The former implements the `predict` method that suits our regression task. The latter implements the `train` method. From f1e831a7b8acfcdab524b8325a961fe5491c862c Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sun, 26 Mar 2023 13:43:59 +0200 Subject: [PATCH 097/208] fix bug in backtest target setting --- freqtrade/freqai/data_kitchen.py | 8 ++++---- freqtrade/freqai/freqai_interface.py | 7 +++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 52d487b08..21b41db2d 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -1291,7 +1291,7 @@ class FreqaiDataKitchen: return dataframe - def use_strategy_to_populate_indicators( + def use_strategy_to_populate_indicators( # noqa: C901 self, strategy: IStrategy, corr_dataframes: dict = {}, @@ -1362,12 +1362,12 @@ class FreqaiDataKitchen: dataframe = self.populate_features(dataframe.copy(), corr_pair, strategy, corr_dataframes, base_dataframes, True) - dataframe = strategy.set_freqai_targets(dataframe.copy(), metadata=metadata) + if self.live: + dataframe = strategy.set_freqai_targets(dataframe.copy(), metadata=metadata) + dataframe = self.remove_special_chars_from_feature_names(dataframe) self.get_unique_classes_from_labels(dataframe) - dataframe = self.remove_special_chars_from_feature_names(dataframe) - if self.config.get('reduce_df_footprint', False): dataframe = reduce_dataframe_footprint(dataframe) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index b657bd811..fe62adea9 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -306,7 +306,7 @@ class IFreqaiModel(ABC): if check_features: self.dd.load_metadata(dk) dataframe_dummy_features = self.dk.use_strategy_to_populate_indicators( - strategy, prediction_dataframe=dataframe.tail(1), pair=metadata["pair"] + strategy, prediction_dataframe=dataframe.tail(1), pair=pair ) dk.find_features(dataframe_dummy_features) self.check_if_feature_list_matches_strategy(dk) @@ -316,7 +316,7 @@ class IFreqaiModel(ABC): else: if populate_indicators: dataframe = self.dk.use_strategy_to_populate_indicators( - strategy, prediction_dataframe=dataframe, pair=metadata["pair"] + strategy, prediction_dataframe=dataframe, pair=pair ) populate_indicators = False @@ -332,6 +332,9 @@ class IFreqaiModel(ABC): dataframe_train = dk.slice_dataframe(tr_train, dataframe_base_train) dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe_base_backtest) + dataframe_train = dk.remove_special_chars_from_feature_names(dataframe_train) + dataframe_backtest = dk.remove_special_chars_from_feature_names(dataframe_backtest) + if not self.model_exists(dk): dk.find_features(dataframe_train) dk.find_labels(dataframe_train) From 55781e7f10fc66aaae4a0039766a8bde94a82355 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sun, 26 Mar 2023 19:22:52 +0200 Subject: [PATCH 098/208] fix tests --- tests/freqai/conftest.py | 2 ++ tests/freqai/test_freqai_datadrawer.py | 7 ++++++- tests/freqai/test_freqai_datakitchen.py | 1 + tests/freqai/test_freqai_interface.py | 6 ++++++ 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index e140ee80b..75034767d 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -119,6 +119,7 @@ def make_unfiltered_dataframe(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True freqai.dk.pair = "ADA/BTC" data_load_timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(data_load_timerange, freqai.dk) @@ -152,6 +153,7 @@ def make_data_dictionary(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True freqai.dk.pair = "ADA/BTC" data_load_timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(data_load_timerange, freqai.dk) diff --git a/tests/freqai/test_freqai_datadrawer.py b/tests/freqai/test_freqai_datadrawer.py index da3b8f9c1..8ab2c75da 100644 --- a/tests/freqai/test_freqai_datadrawer.py +++ b/tests/freqai/test_freqai_datadrawer.py @@ -19,6 +19,7 @@ def test_update_historic_data(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180114") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -41,6 +42,7 @@ def test_load_all_pairs_histories(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180114") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -60,6 +62,7 @@ def test_get_base_and_corr_dataframes(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180114") freqai.dd.load_all_pair_histories(timerange, freqai.dk) sub_timerange = TimeRange.parse_timerange("20180111-20180114") @@ -87,6 +90,7 @@ def test_use_strategy_to_populate_indicators(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180114") freqai.dd.load_all_pair_histories(timerange, freqai.dk) sub_timerange = TimeRange.parse_timerange("20180111-20180114") @@ -103,8 +107,9 @@ def test_get_timerange_from_live_historic_predictions(mocker, freqai_conf): exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) freqai = strategy.freqai - freqai.live = True + freqai.live = False freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = False timerange = TimeRange.parse_timerange("20180126-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) sub_timerange = TimeRange.parse_timerange("20180128-20180130") diff --git a/tests/freqai/test_freqai_datakitchen.py b/tests/freqai/test_freqai_datakitchen.py index 95665a775..3f0fc697d 100644 --- a/tests/freqai/test_freqai_datakitchen.py +++ b/tests/freqai/test_freqai_datakitchen.py @@ -180,6 +180,7 @@ def test_get_full_model_path(mocker, freqai_conf, model): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 3b370aea4..e149ca517 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -87,6 +87,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, freqai.live = True freqai.can_short = can_short freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True freqai.dk.set_paths('ADA/BTC', 10000) timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -135,6 +136,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -178,6 +180,7 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -238,6 +241,7 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog) freqai = strategy.freqai freqai.live = False freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) sub_timerange = TimeRange.parse_timerange("20180110-20180130") @@ -394,6 +398,7 @@ def test_principal_component_analysis(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -425,6 +430,7 @@ def test_plot_feature_importance(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) From 3cabcabcbd5230de3a9c8cb45759d9e540ea4c4f Mon Sep 17 00:00:00 2001 From: robcaulk Date: Mon, 27 Mar 2023 15:23:01 +0200 Subject: [PATCH 099/208] ensure labels are properly defined in backtesting --- freqtrade/freqai/freqai_interface.py | 1 + tests/freqai/test_freqai_interface.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index fe62adea9..7444bf404 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -334,6 +334,7 @@ class IFreqaiModel(ABC): dataframe_train = dk.remove_special_chars_from_feature_names(dataframe_train) dataframe_backtest = dk.remove_special_chars_from_feature_names(dataframe_backtest) + dk.get_unique_classes_from_labels(dataframe_train) if not self.model_exists(dk): dk.find_features(dataframe_train) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index e149ca517..5f8071446 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -241,7 +241,6 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog) freqai = strategy.freqai freqai.live = False freqai.dk = FreqaiDataKitchen(freqai_conf) - freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) sub_timerange = TimeRange.parse_timerange("20180110-20180130") @@ -375,6 +374,9 @@ def test_backtesting_fit_live_predictions(mocker, freqai_conf, caplog): sub_timerange = TimeRange.parse_timerange("20180129-20180130") corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk) df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC") + df = strategy.set_freqai_targets(df.copy(), metadata={"pair": "LTC/BTC"}) + df = freqai.dk.remove_special_chars_from_feature_names(df) + freqai.dk.get_unique_classes_from_labels(df) freqai.dk.pair = "ADA/BTC" freqai.dk.full_df = df.fillna(0) freqai.dk.full_df From 026b6a39a9e91ecc8f2827b73db3e8429b55292e Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 28 Mar 2023 14:40:23 +0300 Subject: [PATCH 100/208] bugfix skip test split when empty --- .../base_models/BasePyTorchClassifier.py | 2 +- .../freqai/base_models/BasePyTorchModel.py | 2 ++ .../prediction_models/PyTorchMLPClassifier.py | 2 +- .../prediction_models/PyTorchMLPRegressor.py | 2 +- freqtrade/freqai/torch/PyTorchModelTrainer.py | 35 ++++++++++++------- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/freqtrade/freqai/base_models/BasePyTorchClassifier.py b/freqtrade/freqai/base_models/BasePyTorchClassifier.py index c08142876..7795b37ce 100644 --- a/freqtrade/freqai/base_models/BasePyTorchClassifier.py +++ b/freqtrade/freqai/base_models/BasePyTorchClassifier.py @@ -97,7 +97,7 @@ class BasePyTorchClassifier(BasePyTorchModel): """ target_column_name = dk.label_list[0] - for split in ["train", "test"]: + for split in self.splits: label_df = data_dictionary[f"{split}_labels"] self.assert_valid_class_names(label_df[target_column_name], class_names) label_df[target_column_name] = list( diff --git a/freqtrade/freqai/base_models/BasePyTorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py index d6372fa36..189f7d906 100644 --- a/freqtrade/freqai/base_models/BasePyTorchModel.py +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -22,6 +22,8 @@ class BasePyTorchModel(IFreqaiModel): super().__init__(config=kwargs["config"]) self.dd.model_type = "pytorch" self.device = "cuda" if torch.cuda.is_available() else "cpu" + test_size = self.freqai_info.get('data_split_parameters', {}).get('test_size') + self.splits = ["train", "test"] if test_size != 0 else ["train"] def train( self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 20c0b0c65..389aa6155 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -76,5 +76,5 @@ class PyTorchMLPClassifier(BasePyTorchClassifier): squeeze_target_tensor=True, **self.trainer_kwargs, ) - trainer.fit(data_dictionary) + trainer.fit(data_dictionary, self.splits) return trainer diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index df149ffbf..ca6a13f6e 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -72,5 +72,5 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): target_tensor_type=torch.float, **self.trainer_kwargs, ) - trainer.fit(data_dictionary) + trainer.fit(data_dictionary, self.splits) return trainer diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index 2ef4b57c9..609e19eda 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -1,7 +1,7 @@ import logging import math from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional import pandas as pd import torch @@ -43,7 +43,6 @@ class PyTorchModelTrainer: self.optimizer.step(). used to calculate n_epochs. :param batch_size: The size of the batches to use during training. :param max_n_eval_batches: The maximum number batches to use for evaluation. - """ self.model = model self.optimizer = optimizer @@ -58,21 +57,27 @@ class PyTorchModelTrainer: if init_model: self.load_from_checkpoint(init_model) - def fit(self, data_dictionary: Dict[str, pd.DataFrame]): + def fit(self, data_dictionary: Dict[str, pd.DataFrame], splits: List[str]): """ + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + :param splits: splits to use in training, splits must contain "train", + optional "test" could be added by setting freqai.data_split_parameters.test_size > 0 + in the config file. + - Calculates the predicted output for the batch using the PyTorch model. - Calculates the loss between the predicted and actual output using a loss function. - Computes the gradients of the loss with respect to the model's parameters using backpropagation. - Updates the model's parameters using an optimizer. """ - data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary) + data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary, splits) epochs = self.calc_n_epochs( n_obs=len(data_dictionary["train_features"]), batch_size=self.batch_size, n_iters=self.max_iters ) - for epoch in range(epochs): + for epoch in range(1, epochs+1): # training losses = [] for i, batch_data in enumerate(data_loaders_dictionary["train"]): @@ -87,13 +92,18 @@ class PyTorchModelTrainer: self.optimizer.step() losses.append(loss.item()) train_loss = sum(losses) / len(losses) + log_message = f"epoch {epoch}/{epochs}: train loss {train_loss:.4f}" # evaluation - test_loss = self.estimate_loss(data_loaders_dictionary, self.max_n_eval_batches, "test") - logger.info( - f"epoch {epoch}/{epochs}:" - f" train loss {train_loss:.4f} ; test loss {test_loss:.4f}" - ) + if "test" in splits: + test_loss = self.estimate_loss( + data_loaders_dictionary, + self.max_n_eval_batches, + "test" + ) + log_message += f" ; test loss {test_loss:.4f}" + + logger.info(log_message) @torch.no_grad() def estimate_loss( @@ -122,13 +132,14 @@ class PyTorchModelTrainer: def create_data_loaders_dictionary( self, - data_dictionary: Dict[str, pd.DataFrame] + data_dictionary: Dict[str, pd.DataFrame], + splits: List[str] ) -> Dict[str, DataLoader]: """ Converts the input data to PyTorch tensors using a data loader. """ data_loader_dictionary = {} - for split in ["train", "test"]: + for split in splits: x = torch.from_numpy(data_dictionary[f"{split}_features"].values).float() y = torch.from_numpy(data_dictionary[f"{split}_labels"].values)\ .to(self.target_tensor_type) From b795a70102bbc6b0c26a57b0d7d87586500ac747 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 28 Mar 2023 14:41:25 +0300 Subject: [PATCH 101/208] fix config example in pytorch mlp documentation --- freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py | 4 ++-- freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 389aa6155..a44214367 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -26,7 +26,7 @@ class PyTorchMLPClassifier(BasePyTorchClassifier): "trainer_kwargs": { "max_iters": 5000, "batch_size": 64, - "max_n_eval_batches": None, + "max_n_eval_batches": null, }, "model_kwargs": { "hidden_dim": 512, @@ -49,7 +49,7 @@ class PyTorchMLPClassifier(BasePyTorchClassifier): """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + all the training and test data/labels. :raises ValueError: If self.class_names is not defined in the parent class. """ diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index ca6a13f6e..6fc2be1a5 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -27,7 +27,7 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): "trainer_kwargs": { "max_iters": 5000, "batch_size": 64, - "max_n_eval_batches": None, + "max_n_eval_batches": null, }, "model_kwargs": { "hidden_dim": 512, @@ -50,7 +50,7 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + all the training and test data/labels. """ n_features = data_dictionary["train_features"].shape[-1] From dfbebdea9bf328f4946295de00ad0830fa643efc Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 28 Mar 2023 14:42:52 +0300 Subject: [PATCH 102/208] improve comment on class_names in freqai interface --- freqtrade/freqai/freqai_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 470ae1911..9218d130c 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -83,7 +83,7 @@ class IFreqaiModel(ABC): self.CONV_WIDTH = self.freqai_info.get('conv_width', 1) if self.ft_params.get("inlier_metric_window", 0): self.CONV_WIDTH = self.ft_params.get("inlier_metric_window", 0) * 2 - self.class_names: List[str] = [] # used in classification children classes + self.class_names: List[str] = [] # used in classification subclasses self.pair_it = 0 self.pair_it_train = 0 self.total_pairs = len(self.config.get("exchange", {}).get("pair_whitelist")) From 8ac3a9435865b482977cefcfe3ea438a3b9b4e27 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 28 Mar 2023 14:45:54 +0300 Subject: [PATCH 103/208] add note to pytorch docs - setting class names for classifiers --- docs/freqai-configuration.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 7b08dd67c..442705b53 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -325,4 +325,18 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): return trainer ``` -Here we create `PyTorchMLPRegressor` that implements the `fit` method. The `fit` method specifies the training building blocks: model, optimizer, criterion, and trainer. We inherit both `BasePyTorchRegressor` and `BasePyTorchModel` (a parent of `BasePyTorchRegressor`). The former implements the `predict` method that suits our regression task. The latter implements the `train` method. +Here we create a `PyTorchMLPRegressor` class that implements the `fit` method. The `fit` method specifies the training building blocks: model, optimizer, criterion, and trainer. We inherit both `BasePyTorchRegressor` and `BasePyTorchModel`, where the former implements the `predict` method that is suitable for our regression task, and the latter implements the train method. + +??? Note "Setting Class Names for Classifiers" + When using classifiers, the user must declare the class names (or targets) by overriding the `IFreqaiModel.class_names` attribute. This is achieved by setting `self.freqai.class_names` in the FreqAI strategy inside the `set_freqai_targets` method. + + For example, if you are using a binary classifier to predict price movements as up or down, you can set the class names as follows: + ```python + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): + self.freqai.class_names = ["down", "up"] + dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) > + dataframe["close"], 'up', 'down') + + return dataframe + ``` + To see a full example, you can refer to the [classifier test strategy class](https://github.com/freqtrade/freqtrade/blob/develop/tests/strategy/strats/freqai_test_classifier.py). From 077a9479728adbc7e341a019b24708e1b552758e Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 28 Mar 2023 15:18:10 +0300 Subject: [PATCH 104/208] clean code --- freqtrade/freqai/torch/PyTorchModelTrainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index 609e19eda..eda880d02 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -77,7 +77,7 @@ class PyTorchModelTrainer: batch_size=self.batch_size, n_iters=self.max_iters ) - for epoch in range(1, epochs+1): + for epoch in range(1, epochs + 1): # training losses = [] for i, batch_data in enumerate(data_loaders_dictionary["train"]): From 5a7ca35c6b4c7c2db68a440b65d7ec4c611674e6 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 28 Mar 2023 16:24:31 +0300 Subject: [PATCH 105/208] declare class names in FreqaiExampleHybridStrategy --- freqtrade/templates/FreqaiExampleHybridStrategy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/templates/FreqaiExampleHybridStrategy.py b/freqtrade/templates/FreqaiExampleHybridStrategy.py index 0e7113f8c..3f27ee4a1 100644 --- a/freqtrade/templates/FreqaiExampleHybridStrategy.py +++ b/freqtrade/templates/FreqaiExampleHybridStrategy.py @@ -223,6 +223,7 @@ class FreqaiExampleHybridStrategy(IStrategy): :param metadata: metadata of current pair usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"] """ + self.freqai.class_names = ["down", "up"] dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-50) > dataframe["close"], 'up', 'down') From 355fde3bca6f668da1fef36e187f85d09cf0b3f7 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 29 Mar 2023 22:01:54 +0200 Subject: [PATCH 106/208] revert setting dk to live in test_plot_feature_importances --- tests/freqai/test_freqai_interface.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 5f8071446..1122d9e9c 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -432,7 +432,6 @@ def test_plot_feature_importance(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) - freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) From cccf4f305bba68f8d90b198616db6ec25f96491f Mon Sep 17 00:00:00 2001 From: initrv Date: Sun, 2 Apr 2023 03:42:05 +0300 Subject: [PATCH 107/208] fix randomize_starting_position typo --- freqtrade/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index ebb946221..1d12ed8c1 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -598,7 +598,7 @@ CONF_SCHEMA = { "model_type": {"type": "string", "default": "PPO"}, "policy_type": {"type": "string", "default": "MlpPolicy"}, "net_arch": {"type": "array", "default": [128, 128]}, - "randomize_startinng_position": {"type": "boolean", "default": False}, + "randomize_starting_position": {"type": "boolean", "default": False}, "model_reward_parameters": { "type": "object", "properties": { From 12a73bc1510baac6a10f5e31bf8340e55eae81ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 03:56:46 +0000 Subject: [PATCH 108/208] Bump websockets from 10.4 to 11.0 Bumps [websockets](https://github.com/aaugustin/websockets) from 10.4 to 11.0. - [Release notes](https://github.com/aaugustin/websockets/releases) - [Commits](https://github.com/aaugustin/websockets/compare/10.4...11.0) --- updated-dependencies: - dependency-name: websockets dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b888d9f6e..f11fc1c42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -53,7 +53,7 @@ python-dateutil==2.8.2 schedule==1.1.0 #WS Messages -websockets==10.4 +websockets==11.0 janus==1.0.0 ast-comments==1.0.1 From b1e20bcd1e30bc4261aa0712cae15072581a5616 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 03:57:00 +0000 Subject: [PATCH 109/208] Bump sqlalchemy from 2.0.7 to 2.0.8 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.7 to 2.0.8. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- updated-dependencies: - dependency-name: sqlalchemy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b888d9f6e..78da706a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pandas-ta==0.3.14b ccxt==3.0.37 cryptography==40.0.1 aiohttp==3.8.4 -SQLAlchemy==2.0.7 +SQLAlchemy==2.0.8 python-telegram-bot==13.15 arrow==1.2.3 cachetools==4.2.2 From 26ed1ca07c77e0c3831df0edfa128ee3a6513113 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 03:57:05 +0000 Subject: [PATCH 110/208] Bump xgboost from 1.7.4 to 1.7.5 Bumps [xgboost](https://github.com/dmlc/xgboost) from 1.7.4 to 1.7.5. - [Release notes](https://github.com/dmlc/xgboost/releases) - [Changelog](https://github.com/dmlc/xgboost/blob/master/NEWS.md) - [Commits](https://github.com/dmlc/xgboost/compare/v1.7.4...v1.7.5) --- updated-dependencies: - dependency-name: xgboost dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-freqai.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index e6eae667c..e3a811661 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -7,5 +7,5 @@ scikit-learn==1.1.3 joblib==1.2.0 catboost==1.1.1; platform_machine != 'aarch64' and 'arm' not in platform_machine and python_version < '3.11' lightgbm==3.3.5 -xgboost==1.7.4 +xgboost==1.7.5 tensorboard==2.12.0 From e289c10b6cc1b00d1ceee48a10ca965b82b858f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 03:57:10 +0000 Subject: [PATCH 111/208] Bump types-cachetools from 5.3.0.4 to 5.3.0.5 Bumps [types-cachetools](https://github.com/python/typeshed) from 5.3.0.4 to 5.3.0.5. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-cachetools dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 3324c11e9..e43048cb9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -25,7 +25,7 @@ httpx==0.23.3 nbconvert==7.2.10 # mypy types -types-cachetools==5.3.0.4 +types-cachetools==5.3.0.5 types-filelock==3.2.7 types-requests==2.28.11.16 types-tabulate==0.9.0.1 From 1b31c5416287a22abc399543054b0e139817a967 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 03:57:19 +0000 Subject: [PATCH 112/208] Bump ccxt from 3.0.37 to 3.0.50 Bumps [ccxt](https://github.com/ccxt/ccxt) from 3.0.37 to 3.0.50. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/3.0.37...3.0.50) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b888d9f6e..d3df857da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.2 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==3.0.37 +ccxt==3.0.50 cryptography==40.0.1 aiohttp==3.8.4 SQLAlchemy==2.0.7 From 2ea575cb310428c556698a3748b9efb1459dd7ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 03:57:30 +0000 Subject: [PATCH 113/208] Bump mkdocs-material from 9.1.4 to 9.1.5 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.4 to 9.1.5. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.1.4...9.1.5) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 7f4215aef..c70415c85 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.2 -mkdocs-material==9.1.4 +mkdocs-material==9.1.5 mdx_truly_sane_lists==1.3 pymdown-extensions==9.10 jinja2==3.1.2 From 2715b2ccf0a43f38dd585af6ee190e27a2bf10fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 03:58:12 +0000 Subject: [PATCH 114/208] Bump pypa/gh-action-pypi-publish from 1.8.3 to 1.8.4 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.3 to 1.8.4. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.3...v1.8.4) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c80bc141..e856607fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -425,7 +425,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@v1.8.3 + uses: pypa/gh-action-pypi-publish@v1.8.4 if: (github.event_name == 'release') with: user: __token__ @@ -433,7 +433,7 @@ jobs: repository_url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.3 + uses: pypa/gh-action-pypi-publish@v1.8.4 if: (github.event_name == 'release') with: user: __token__ From 6f79d14c9ccfa8d9375f33acd7e936ccab3e8a0c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Apr 2023 06:37:15 +0200 Subject: [PATCH 115/208] pre-commit - bump cachetools --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4784055a9..142d8d50d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: - id: mypy exclude: build_helpers additional_dependencies: - - types-cachetools==5.3.0.4 + - types-cachetools==5.3.0.5 - types-filelock==3.2.7 - types-requests==2.28.11.16 - types-tabulate==0.9.0.1 From 78a1551798abb05687822050be8efa08774cce56 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Apr 2023 16:45:42 +0200 Subject: [PATCH 116/208] Reorder get_stake_limit --- freqtrade/exchange/exchange.py | 36 ++++++++++++++-------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 437ed4289..2d2fa3354 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -788,27 +788,6 @@ class Exchange: except KeyError: raise ValueError(f"Can't get market information for symbol {pair}") - stake_limits = [] - limits = market['limits'] - if (limits['cost'][limit] is not None): - stake_limits.append( - self._contracts_to_amount( - pair, - limits['cost'][limit] - ) - ) - - if (limits['amount'][limit] is not None): - stake_limits.append( - self._contracts_to_amount( - pair, - limits['amount'][limit] * price - ) - ) - - if not stake_limits: - return None if isMin else float('inf') - # reserve some percent defined in config (5% default) + stoploss amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent', DEFAULT_AMOUNT_RESERVE_PERCENT) @@ -818,6 +797,21 @@ class Exchange: # it should not be more than 50% amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) + stake_limits = [] + limits = market['limits'] + if (limits['cost'][limit] is not None): + stake_limits.append( + self._contracts_to_amount(pair, limits['cost'][limit]) + ) + + if (limits['amount'][limit] is not None): + stake_limits.append( + self._contracts_to_amount(pair, limits['amount'][limit] * price) + ) + + if not stake_limits: + return None if isMin else float('inf') + # The value returned should satisfy both limits: for amount (base currency) and # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. From e6a125719e09acaf5d03948b24142cad8c49bb6b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Apr 2023 16:59:45 +0200 Subject: [PATCH 117/208] Slightly refactor _get_stake_amount_limit --- freqtrade/exchange/exchange.py | 8 ++++---- tests/exchange/test_exchange.py | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2d2fa3354..1c37ad638 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -765,12 +765,12 @@ class Exchange: return self._get_stake_amount_limit(pair, price, stoploss, 'min', leverage) def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float: - max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max') + max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max', leverage) if max_stake_amount is None: # * Should never be executed raise OperationalException(f'{self.name}.get_max_pair_stake_amount should' 'never set max_stake_amount to None') - return max_stake_amount / leverage + return max_stake_amount def _get_stake_amount_limit( self, @@ -816,9 +816,9 @@ class Exchange: # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. return self._get_stake_amount_considering_leverage( - max(stake_limits) * amount_reserve_percent, + (max(stake_limits) * amount_reserve_percent) if isMin else min(stake_limits), leverage or 1.0 - ) if isMin else min(stake_limits) + ) def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float: """ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8c9d83a96..0325e6204 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -496,6 +496,9 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: result = exchange.get_max_pair_stake_amount('ETH/BTC', 2) assert result == 1000 + result = exchange.get_max_pair_stake_amount('ETH/BTC', 2, 12.0) + assert result == 1000 / 12 + markets["ETH/BTC"]["contractSize"] = '0.01' default_conf['trading_mode'] = 'futures' default_conf['margin_mode'] = 'isolated' From a3acdd52406b33e83dc17498b7366f300ee1c054 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Apr 2023 17:10:04 +0200 Subject: [PATCH 118/208] apply stop-reserve to minimum limits only when necessary it's unnecessary for amount - but necessary for Cost / price limits. --- freqtrade/exchange/exchange.py | 26 +++++++++++++++----------- tests/exchange/test_exchange.py | 6 +++--- tests/test_freqtradebot.py | 2 +- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 1c37ad638..6c236106f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -788,25 +788,29 @@ class Exchange: except KeyError: raise ValueError(f"Can't get market information for symbol {pair}") - # reserve some percent defined in config (5% default) + stoploss - amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent', - DEFAULT_AMOUNT_RESERVE_PERCENT) - amount_reserve_percent = ( - amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 - ) - # it should not be more than 50% - amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) + if isMin: + # reserve some percent defined in config (5% default) + stoploss + margin_reserve: float = 1.0 + self._config.get('amount_reserve_percent', + DEFAULT_AMOUNT_RESERVE_PERCENT) + stoploss_reserve = ( + margin_reserve / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 + ) + # it should not be more than 50% + stoploss_reserve = max(min(stoploss_reserve, 1.5), 1) + else: + margin_reserve = 1.0 + stoploss_reserve = 1.0 stake_limits = [] limits = market['limits'] if (limits['cost'][limit] is not None): stake_limits.append( - self._contracts_to_amount(pair, limits['cost'][limit]) + self._contracts_to_amount(pair, limits['cost'][limit]) * stoploss_reserve ) if (limits['amount'][limit] is not None): stake_limits.append( - self._contracts_to_amount(pair, limits['amount'][limit] * price) + self._contracts_to_amount(pair, limits['amount'][limit]) * price * margin_reserve ) if not stake_limits: @@ -816,7 +820,7 @@ class Exchange: # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. return self._get_stake_amount_considering_leverage( - (max(stake_limits) * amount_reserve_percent) if isMin else min(stake_limits), + max(stake_limits) if isMin else min(stake_limits), leverage or 1.0 ) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 0325e6204..fcc3dd4f8 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -437,7 +437,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: } mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - expected_result = 2 * 2 * (1 + 0.05) / (1 - abs(stoploss)) + expected_result = 2 * 2 * (1 + 0.05) assert pytest.approx(result) == expected_result # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0) @@ -446,14 +446,14 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: result = exchange.get_max_pair_stake_amount('ETH/BTC', 2) assert result == 20000 - # min amount and cost are set (cost is minimal) + # min amount and cost are set (cost is minimal and therefore ignored) markets["ETH/BTC"]["limits"] = { 'cost': {'min': 2, 'max': None}, 'amount': {'min': 2, 'max': None}, } mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - expected_result = max(2, 2 * 2) * (1 + 0.05) / (1 - abs(stoploss)) + expected_result = max(2, 2 * 2) * (1 + 0.05) assert pytest.approx(result) == expected_result # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5dc3a993c..ab5dd4af5 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -356,7 +356,7 @@ def test_create_trade_no_stake_amount(default_conf_usdt, ticker_usdt, fee, mocke @pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [ (5.0, True, True, 99), - (0.049, True, False, 99), # Amount will be adjusted to min - which is 0.051 + (0.042, True, False, 99), # Amount will be adjusted to min - which is 0.051 (0, False, True, 99), (UNLIMITED_STAKE_AMOUNT, False, True, 0), ]) From 372f1cb37f4beaa4f0cd115177b08cbf0dd74200 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Apr 2023 17:29:32 +0200 Subject: [PATCH 119/208] Reduce verbosity for stop orders --- freqtrade/freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index bd281bc79..af4f42feb 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1783,7 +1783,8 @@ class FreqtradeBot(LoggingMixin): return False # Update trade with order values - logger.info(f'Found open order for {trade}') + if not stoploss_order: + logger.info(f'Found open order for {trade}') try: order = action_order or self.exchange.fetch_order_or_stoploss_order(order_id, trade.pair, From c9b904eb0e047bacdb8f717a21c3f9eeafcd63be Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Apr 2023 06:49:30 +0200 Subject: [PATCH 120/208] Fix typos in documentation --- docs/freqai-feature-engineering.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/freqai-feature-engineering.md b/docs/freqai-feature-engineering.md index 1ca25d15c..05c6db523 100644 --- a/docs/freqai-feature-engineering.md +++ b/docs/freqai-feature-engineering.md @@ -6,8 +6,8 @@ Low level feature engineering is performed in the user strategy within a set of | Function | Description | |---------------|-------------| -| `feature_engineering__expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. -| `feature_engineering__expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`. +| `feature_engineering_expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. +| `feature_engineering_expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`. | `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g., day of the week). | `set_freqai_targets()` | Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals. From 43496d79290cef622d59591d3fd2edae533bb0d3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Apr 2023 09:46:32 +0200 Subject: [PATCH 121/208] bump sqlalchemy pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4784055a9..ca8609982 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - types-requests==2.28.11.16 - types-tabulate==0.9.0.1 - types-python-dateutil==2.8.19.10 - - SQLAlchemy==2.0.7 + - SQLAlchemy==2.0.8 # stages: [push] - repo: https://github.com/pycqa/isort From 8236bbfd482b070649a52031b467ad5466a35bb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 07:48:43 +0000 Subject: [PATCH 122/208] Bump orjson from 3.8.8 to 3.8.9 Bumps [orjson](https://github.com/ijl/orjson) from 3.8.8 to 3.8.9. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.8.8...3.8.9) --- updated-dependencies: - dependency-name: orjson dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9129f0c2b..8899ed7e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.10 # Properly format api responses -orjson==3.8.8 +orjson==3.8.9 # Notify systemd sdnotify==0.3.2 From bf7936b0af51e1eaf58145a256bddb60c2129730 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 07:48:50 +0000 Subject: [PATCH 123/208] Bump plotly from 5.13.1 to 5.14.0 Bumps [plotly](https://github.com/plotly/plotly.py) from 5.13.1 to 5.14.0. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v5.13.1...v5.14.0) --- updated-dependencies: - dependency-name: plotly dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index ad7bade95..d87219c42 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,4 +1,4 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==5.13.1 +plotly==5.14.0 From 2bd2058afa97ed0c889e08fe6ee366812bfa4a7e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 07:49:12 +0000 Subject: [PATCH 124/208] Bump ruff from 0.0.259 to 0.0.260 Bumps [ruff](https://github.com/charliermarsh/ruff) from 0.0.259 to 0.0.260. - [Release notes](https://github.com/charliermarsh/ruff/releases) - [Changelog](https://github.com/charliermarsh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/charliermarsh/ruff/compare/v0.0.259...v0.0.260) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e43048cb9..04cf5c7c9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.0.259 +ruff==0.0.260 mypy==1.1.1 pre-commit==3.2.1 pytest==7.2.2 From 7779b82277c0be0495ac90b444c5718fe76aecfd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 07:49:18 +0000 Subject: [PATCH 125/208] Bump tensorboard from 2.12.0 to 2.12.1 Bumps [tensorboard](https://github.com/tensorflow/tensorboard) from 2.12.0 to 2.12.1. - [Release notes](https://github.com/tensorflow/tensorboard/releases) - [Changelog](https://github.com/tensorflow/tensorboard/blob/2.12.1/RELEASE.md) - [Commits](https://github.com/tensorflow/tensorboard/compare/2.12.0...2.12.1) --- updated-dependencies: - dependency-name: tensorboard dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-freqai.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index e3a811661..840598d23 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -8,4 +8,4 @@ joblib==1.2.0 catboost==1.1.1; platform_machine != 'aarch64' and 'arm' not in platform_machine and python_version < '3.11' lightgbm==3.3.5 xgboost==1.7.5 -tensorboard==2.12.0 +tensorboard==2.12.1 From 57deaad8065b7bd43a84e6f9cdfa9eae0a763ca0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 07:49:21 +0000 Subject: [PATCH 126/208] Bump types-requests from 2.28.11.16 to 2.28.11.17 Bumps [types-requests](https://github.com/python/typeshed) from 2.28.11.16 to 2.28.11.17. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e43048cb9..b7a1b4062 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -27,6 +27,6 @@ nbconvert==7.2.10 # mypy types types-cachetools==5.3.0.5 types-filelock==3.2.7 -types-requests==2.28.11.16 +types-requests==2.28.11.17 types-tabulate==0.9.0.1 types-python-dateutil==2.8.19.10 From ff40ee655bbf87bc31fa5f054bffb51cbd4f8a84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 07:49:24 +0000 Subject: [PATCH 127/208] Bump types-python-dateutil from 2.8.19.10 to 2.8.19.11 Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.19.10 to 2.8.19.11. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-python-dateutil dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e43048cb9..95b1d2558 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -29,4 +29,4 @@ types-cachetools==5.3.0.5 types-filelock==3.2.7 types-requests==2.28.11.16 types-tabulate==0.9.0.1 -types-python-dateutil==2.8.19.10 +types-python-dateutil==2.8.19.11 From b48498f27fe8bb9fae12127d9696fdec2a3e7a70 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Apr 2023 10:16:56 +0200 Subject: [PATCH 128/208] Types pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 142d8d50d..425b862c9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: additional_dependencies: - types-cachetools==5.3.0.5 - types-filelock==3.2.7 - - types-requests==2.28.11.16 + - types-requests==2.28.11.17 - types-tabulate==0.9.0.1 - types-python-dateutil==2.8.19.10 - SQLAlchemy==2.0.7 From b96f6670e3845336f2c744f9f0fdb7e57ff79218 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Apr 2023 13:28:17 +0200 Subject: [PATCH 129/208] pre-commit dateutil --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 142d8d50d..a15c53dc2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - types-filelock==3.2.7 - types-requests==2.28.11.16 - types-tabulate==0.9.0.1 - - types-python-dateutil==2.8.19.10 + - types-python-dateutil==2.8.19.11 - SQLAlchemy==2.0.7 # stages: [push] From 30fc24bd8c39805d729092cbe4917b9384c1ce93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 12:18:15 +0000 Subject: [PATCH 130/208] Bump types-tabulate from 0.9.0.1 to 0.9.0.2 Bumps [types-tabulate](https://github.com/python/typeshed) from 0.9.0.1 to 0.9.0.2. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-tabulate dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index eec8f81b6..f36ef6def 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -28,5 +28,5 @@ nbconvert==7.2.10 types-cachetools==5.3.0.5 types-filelock==3.2.7 types-requests==2.28.11.17 -types-tabulate==0.9.0.1 +types-tabulate==0.9.0.2 types-python-dateutil==2.8.19.11 From bd3b70293f3227facb476e6a0eb1f090009a26cc Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 3 Apr 2023 15:19:10 +0300 Subject: [PATCH 131/208] add pytorch data convertor --- .../base_models/BasePyTorchClassifier.py | 9 ++- .../freqai/base_models/BasePyTorchModel.py | 12 +++- .../base_models/BasePyTorchRegressor.py | 10 ++-- .../prediction_models/PyTorchMLPClassifier.py | 9 ++- .../prediction_models/PyTorchMLPRegressor.py | 8 ++- .../freqai/torch/PyTorchDataConvertor.py | 56 +++++++++++++++++++ freqtrade/freqai/torch/PyTorchMLPModel.py | 4 +- freqtrade/freqai/torch/PyTorchModelTrainer.py | 46 +++++++-------- .../freqai/torch/PyTorchTrainerInterface.py | 54 ++++++++++++++++++ 9 files changed, 168 insertions(+), 40 deletions(-) create mode 100644 freqtrade/freqai/torch/PyTorchDataConvertor.py create mode 100644 freqtrade/freqai/torch/PyTorchTrainerInterface.py diff --git a/freqtrade/freqai/base_models/BasePyTorchClassifier.py b/freqtrade/freqai/base_models/BasePyTorchClassifier.py index 7795b37ce..977152cc5 100644 --- a/freqtrade/freqai/base_models/BasePyTorchClassifier.py +++ b/freqtrade/freqai/base_models/BasePyTorchClassifier.py @@ -69,12 +69,11 @@ class BasePyTorchClassifier(BasePyTorchModel): ) filtered_df = dk.normalize_data_from_metadata(filtered_df) dk.data_dictionary["prediction_features"] = filtered_df - self.data_cleaning_predict(dk) - x = torch.from_numpy(dk.data_dictionary["prediction_features"].values)\ - .float()\ - .to(self.device) - + x = self.data_convertor.convert_x( + dk.data_dictionary["prediction_features"], + device=self.device + ) logits = self.model.model(x) probs = F.softmax(logits, dim=-1) predicted_classes = torch.argmax(probs, dim=-1) diff --git a/freqtrade/freqai/base_models/BasePyTorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py index 189f7d906..7b968c762 100644 --- a/freqtrade/freqai/base_models/BasePyTorchModel.py +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -1,4 +1,5 @@ import logging +from abc import ABC, abstractmethod from time import time from typing import Any @@ -7,15 +8,17 @@ from pandas import DataFrame from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.freqai_interface import IFreqaiModel +from freqtrade.freqai.torch import PyTorchDataConvertor logger = logging.getLogger(__name__) -class BasePyTorchModel(IFreqaiModel): +class BasePyTorchModel(IFreqaiModel, ABC): """ Base class for PyTorch type models. - User *must* inherit from this class and set fit() and predict(). + User *must* inherit from this class and set fit() and predict() and + data_convertor property. """ def __init__(self, **kwargs): @@ -69,3 +72,8 @@ class BasePyTorchModel(IFreqaiModel): f"({end_time - start_time:.2f} secs) --------------------") return model + + @property + @abstractmethod + def data_convertor(self) -> PyTorchDataConvertor: + raise NotImplementedError("Abstract property") diff --git a/freqtrade/freqai/base_models/BasePyTorchRegressor.py b/freqtrade/freqai/base_models/BasePyTorchRegressor.py index 756853496..bf6f86041 100644 --- a/freqtrade/freqai/base_models/BasePyTorchRegressor.py +++ b/freqtrade/freqai/base_models/BasePyTorchRegressor.py @@ -3,7 +3,6 @@ from typing import Tuple import numpy as np import numpy.typing as npt -import torch from pandas import DataFrame from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel @@ -41,9 +40,12 @@ class BasePyTorchRegressor(BasePyTorchModel): dk.data_dictionary["prediction_features"] = filtered_df self.data_cleaning_predict(dk) - x = torch.from_numpy(dk.data_dictionary["prediction_features"].values)\ - .float()\ - .to(self.device) + x = self.data_convertor.convert_x( + dk.data_dictionary["prediction_features"], + device=self.device + ) + logger.info(self.model.model) + logger.info(self.model.model) y = self.model.model(x) pred_df = DataFrame(y.detach().numpy(), columns=[dk.label_list[0]]) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index a44214367..5b7ea462e 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -4,6 +4,8 @@ import torch from freqtrade.freqai.base_models.BasePyTorchClassifier import BasePyTorchClassifier from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.torch import PyTorchDataConvertor +from freqtrade.freqai.torch.PyTorchDataConvertor import DefaultPyTorchDataConvertor from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer @@ -38,6 +40,10 @@ class PyTorchMLPClassifier(BasePyTorchClassifier): } """ + @property + def data_convertor(self) -> PyTorchDataConvertor: + return DefaultPyTorchDataConvertor(target_tensor_type=torch.long, squeeze_target_tensor=True) + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) config = self.freqai_info.get("model_training_parameters", {}) @@ -72,8 +78,7 @@ class PyTorchMLPClassifier(BasePyTorchClassifier): model_meta_data={"class_names": class_names}, device=self.device, init_model=init_model, - target_tensor_type=torch.long, - squeeze_target_tensor=True, + data_convertor=self.data_convertor, **self.trainer_kwargs, ) trainer.fit(data_dictionary, self.splits) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 6fc2be1a5..326f14994 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -4,6 +4,8 @@ import torch from freqtrade.freqai.base_models.BasePyTorchRegressor import BasePyTorchRegressor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.torch import PyTorchDataConvertor +from freqtrade.freqai.torch.PyTorchDataConvertor import DefaultPyTorchDataConvertor from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer @@ -39,6 +41,10 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): } """ + @property + def data_convertor(self) -> PyTorchDataConvertor: + return DefaultPyTorchDataConvertor(target_tensor_type=torch.float) + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) config = self.freqai_info.get("model_training_parameters", {}) @@ -69,7 +75,7 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): criterion=criterion, device=self.device, init_model=init_model, - target_tensor_type=torch.float, + data_convertor=self.data_convertor, **self.trainer_kwargs, ) trainer.fit(data_dictionary, self.splits) diff --git a/freqtrade/freqai/torch/PyTorchDataConvertor.py b/freqtrade/freqai/torch/PyTorchDataConvertor.py new file mode 100644 index 000000000..1c948c72e --- /dev/null +++ b/freqtrade/freqai/torch/PyTorchDataConvertor.py @@ -0,0 +1,56 @@ +from abc import ABC, abstractmethod +from typing import Optional, Tuple + +import pandas as pd +import torch + + +class PyTorchDataConvertor(ABC): + + @abstractmethod + def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: + """ + :param df: "*_features" dataframe. + :param device: cpu/gpu. + :returns: tuple of tensors. + """ + + @abstractmethod + def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: + """ + :param df: "*_labels" dataframe. + :param device: cpu/gpu. + :returns: tuple of tensors. + """ + + +class DefaultPyTorchDataConvertor(PyTorchDataConvertor): + + def __init__( + self, + target_tensor_type: Optional[torch.dtype] = None, + squeeze_target_tensor: bool = False + ): + self._target_tensor_type = target_tensor_type + self._squeeze_target_tensor = squeeze_target_tensor + + def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: + x = torch.from_numpy(df.values).float() + if device: + x = x.to(device) + + return x, + + def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: + y = torch.from_numpy(df.values) + + if self._target_tensor_type: + y = y.to(self._target_tensor_type) + + if self._squeeze_target_tensor: + y = y.squeeze() + + if device: + y = y.to(device) + + return y, diff --git a/freqtrade/freqai/torch/PyTorchMLPModel.py b/freqtrade/freqai/torch/PyTorchMLPModel.py index 22fb9c3f0..2deffd708 100644 --- a/freqtrade/freqai/torch/PyTorchMLPModel.py +++ b/freqtrade/freqai/torch/PyTorchMLPModel.py @@ -1,4 +1,5 @@ import logging +from typing import Tuple, List import torch import torch.nn as nn @@ -46,7 +47,8 @@ class PyTorchMLPModel(nn.Module): self.relu = nn.ReLU() self.dropout = nn.Dropout(p=dropout_percent) - def forward(self, x: torch.Tensor) -> torch.Tensor: + def forward(self, x: List[torch.Tensor]) -> torch.Tensor: + x, = x x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.blocks(x) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index eda880d02..ef5c64a8a 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -9,11 +9,13 @@ import torch.nn as nn from torch.optim import Optimizer from torch.utils.data import DataLoader, TensorDataset +from freqtrade.freqai.torch.PyTorchDataConvertor import PyTorchDataConvertor +from freqtrade.freqai.torch.PyTorchTrainerInterface import PyTorchTrainerInterface logger = logging.getLogger(__name__) -class PyTorchModelTrainer: +class PyTorchModelTrainer(PyTorchTrainerInterface): def __init__( self, model: nn.Module, @@ -21,8 +23,7 @@ class PyTorchModelTrainer: criterion: nn.Module, device: str, init_model: Dict, - target_tensor_type: torch.dtype, - squeeze_target_tensor: bool = False, + data_convertor: PyTorchDataConvertor, model_meta_data: Dict[str, Any] = {}, **kwargs ): @@ -33,11 +34,7 @@ class PyTorchModelTrainer: :param device: The device to use for training (e.g. 'cpu', 'cuda'). :param init_model: A dictionary containing the initial model/optimizer state_dict and model_meta_data saved by self.save() method. - :param target_tensor_type: type of target tensor, for classification usually - torch.long, for regressor usually torch.float. :param model_meta_data: Additional metadata about the model (optional). - :param squeeze_target_tensor: controls the target shape, used for loss functions - that requires 0D or 1D. :param max_iters: The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs. @@ -49,11 +46,10 @@ class PyTorchModelTrainer: self.criterion = criterion self.model_meta_data = model_meta_data self.device = device - self.target_tensor_type = target_tensor_type self.max_iters: int = kwargs.get("max_iters", 100) self.batch_size: int = kwargs.get("batch_size", 64) self.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None) - self.squeeze_target_tensor = squeeze_target_tensor + self.data_convertor = data_convertor if init_model: self.load_from_checkpoint(init_model) @@ -81,9 +77,12 @@ class PyTorchModelTrainer: # training losses = [] for i, batch_data in enumerate(data_loaders_dictionary["train"]): - xb, yb = batch_data - xb = xb.to(self.device) - yb = yb.to(self.device) + + for tensor in batch_data: + tensor.to(self.device) + + xb = batch_data[:-1] + yb = batch_data[-1] yb_pred = self.model(xb) loss = self.criterion(yb_pred, yb) @@ -115,14 +114,16 @@ class PyTorchModelTrainer: self.model.eval() n_batches = 0 losses = [] - for i, batch in enumerate(data_loader_dictionary[split]): + for i, batch_data in enumerate(data_loader_dictionary[split]): if max_n_eval_batches and i > max_n_eval_batches: n_batches += 1 break - xb, yb = batch - xb = xb.to(self.device) - yb = yb.to(self.device) + for tensor in batch_data: + tensor.to(self.device) + + xb = batch_data[:-1] + yb = batch_data[-1] yb_pred = self.model(xb) loss = self.criterion(yb_pred, yb) losses.append(loss.item()) @@ -140,14 +141,9 @@ class PyTorchModelTrainer: """ data_loader_dictionary = {} for split in splits: - x = torch.from_numpy(data_dictionary[f"{split}_features"].values).float() - y = torch.from_numpy(data_dictionary[f"{split}_labels"].values)\ - .to(self.target_tensor_type) - - if self.squeeze_target_tensor: - y = y.squeeze() - - dataset = TensorDataset(x, y) + x = self.data_convertor.convert_x(data_dictionary[f"{split}_features"]) + y = self.data_convertor.convert_y(data_dictionary[f"{split}_labels"]) + dataset = TensorDataset(*x, *y) data_loader = DataLoader( dataset, batch_size=self.batch_size, @@ -186,7 +182,7 @@ class PyTorchModelTrainer: "model_meta_data": self.model_meta_data, }, path) - def load_from_file(self, path: Path): + def load(self, path: Path): checkpoint = torch.load(path) return self.load_from_checkpoint(checkpoint) diff --git a/freqtrade/freqai/torch/PyTorchTrainerInterface.py b/freqtrade/freqai/torch/PyTorchTrainerInterface.py new file mode 100644 index 000000000..2924f2ef9 --- /dev/null +++ b/freqtrade/freqai/torch/PyTorchTrainerInterface.py @@ -0,0 +1,54 @@ +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Optional, Tuple + +import pandas as pd +import torch +import torch.nn as nn + +from pathlib import Path + + +class PyTorchTrainerInterface(ABC): + + @abstractmethod + def fit(self, data_dictionary: Dict[str, pd.DataFrame], splits: List[str]) -> None: + """ + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + :param splits: splits to use in training, splits must contain "train", + optional "test" could be added by setting freqai.data_split_parameters.test_size > 0 + in the config file. + + - Calculates the predicted output for the batch using the PyTorch model. + - Calculates the loss between the predicted and actual output using a loss function. + - Computes the gradients of the loss with respect to the model's parameters using + backpropagation. + - Updates the model's parameters using an optimizer. + """ + + @abstractmethod + def save(self, path: Path) -> None: + """ + - Saving any nn.Module state_dict + - Saving model_meta_data, this dict should contain any additional data that the + user needs to store. e.g class_names for classification models. + """ + + def load(self, path: Path) -> nn.Module: + """ + :param path: path to zip file. + :returns: pytorch model. + """ + checkpoint = torch.load(path) + return self.load_from_checkpoint(checkpoint) + + @abstractmethod + def load_from_checkpoint(self, checkpoint: Dict) -> nn.Module: + """ + when using continual_learning, DataDrawer will load the dictionary + (containing state dicts and model_meta_data) by calling torch.load(path). + you can access this dict from any class that inherits IFreqaiModel by calling + get_init_model method. + :checkpoint checkpoint: dict containing the model & optimizer state dicts, + model_meta_data, etc.. + """ \ No newline at end of file From 7fed0782d5df67e9e7a98a5c18d5c221ea8cb12a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Apr 2023 14:19:11 +0200 Subject: [PATCH 132/208] pre-commit types-tabulate --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f6594e6a..a5ac69ff4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - types-cachetools==5.3.0.5 - types-filelock==3.2.7 - types-requests==2.28.11.17 - - types-tabulate==0.9.0.1 + - types-tabulate==0.9.0.2 - types-python-dateutil==2.8.19.11 - SQLAlchemy==2.0.8 # stages: [push] From c137666230875cdc7fa6af7962382d681f8a4a19 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 3 Apr 2023 16:03:15 +0300 Subject: [PATCH 133/208] fix imports --- freqtrade/freqai/base_models/BasePyTorchModel.py | 2 +- .../freqai/prediction_models/PyTorchMLPClassifier.py | 9 ++++++--- .../freqai/prediction_models/PyTorchMLPRegressor.py | 4 ++-- freqtrade/freqai/torch/PyTorchMLPModel.py | 2 +- freqtrade/freqai/torch/PyTorchModelTrainer.py | 1 + freqtrade/freqai/torch/PyTorchTrainerInterface.py | 7 +++---- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqai/base_models/BasePyTorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py index 7b968c762..d017f1fec 100644 --- a/freqtrade/freqai/base_models/BasePyTorchModel.py +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -8,7 +8,7 @@ from pandas import DataFrame from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.freqai_interface import IFreqaiModel -from freqtrade.freqai.torch import PyTorchDataConvertor +from freqtrade.freqai.torch.PyTorchDataConvertor import PyTorchDataConvertor logger = logging.getLogger(__name__) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 5b7ea462e..8694453be 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -4,8 +4,8 @@ import torch from freqtrade.freqai.base_models.BasePyTorchClassifier import BasePyTorchClassifier from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.torch import PyTorchDataConvertor -from freqtrade.freqai.torch.PyTorchDataConvertor import DefaultPyTorchDataConvertor +from freqtrade.freqai.torch.PyTorchDataConvertor import (DefaultPyTorchDataConvertor, + PyTorchDataConvertor) from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer @@ -42,7 +42,10 @@ class PyTorchMLPClassifier(BasePyTorchClassifier): @property def data_convertor(self) -> PyTorchDataConvertor: - return DefaultPyTorchDataConvertor(target_tensor_type=torch.long, squeeze_target_tensor=True) + return DefaultPyTorchDataConvertor( + target_tensor_type=torch.long, + squeeze_target_tensor=True + ) def __init__(self, **kwargs) -> None: super().__init__(**kwargs) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 326f14994..5ca3486e1 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -4,8 +4,8 @@ import torch from freqtrade.freqai.base_models.BasePyTorchRegressor import BasePyTorchRegressor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.torch import PyTorchDataConvertor -from freqtrade.freqai.torch.PyTorchDataConvertor import DefaultPyTorchDataConvertor +from freqtrade.freqai.torch.PyTorchDataConvertor import (DefaultPyTorchDataConvertor, + PyTorchDataConvertor) from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer diff --git a/freqtrade/freqai/torch/PyTorchMLPModel.py b/freqtrade/freqai/torch/PyTorchMLPModel.py index 2deffd708..01192e115 100644 --- a/freqtrade/freqai/torch/PyTorchMLPModel.py +++ b/freqtrade/freqai/torch/PyTorchMLPModel.py @@ -1,5 +1,5 @@ import logging -from typing import Tuple, List +from typing import List import torch import torch.nn as nn diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index ef5c64a8a..09de6f940 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -12,6 +12,7 @@ from torch.utils.data import DataLoader, TensorDataset from freqtrade.freqai.torch.PyTorchDataConvertor import PyTorchDataConvertor from freqtrade.freqai.torch.PyTorchTrainerInterface import PyTorchTrainerInterface + logger = logging.getLogger(__name__) diff --git a/freqtrade/freqai/torch/PyTorchTrainerInterface.py b/freqtrade/freqai/torch/PyTorchTrainerInterface.py index 2924f2ef9..6686555f9 100644 --- a/freqtrade/freqai/torch/PyTorchTrainerInterface.py +++ b/freqtrade/freqai/torch/PyTorchTrainerInterface.py @@ -1,12 +1,11 @@ from abc import ABC, abstractmethod -from typing import Any, Dict, List, Optional, Tuple +from pathlib import Path +from typing import Dict, List import pandas as pd import torch import torch.nn as nn -from pathlib import Path - class PyTorchTrainerInterface(ABC): @@ -51,4 +50,4 @@ class PyTorchTrainerInterface(ABC): get_init_model method. :checkpoint checkpoint: dict containing the model & optimizer state dicts, model_meta_data, etc.. - """ \ No newline at end of file + """ From 36a0a14a2328db6a06acc42a523ef5cd7007890f Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 3 Apr 2023 16:26:42 +0300 Subject: [PATCH 134/208] clean code --- freqtrade/freqai/base_models/BasePyTorchRegressor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/freqai/base_models/BasePyTorchRegressor.py b/freqtrade/freqai/base_models/BasePyTorchRegressor.py index bf6f86041..b9c5fa685 100644 --- a/freqtrade/freqai/base_models/BasePyTorchRegressor.py +++ b/freqtrade/freqai/base_models/BasePyTorchRegressor.py @@ -44,9 +44,6 @@ class BasePyTorchRegressor(BasePyTorchModel): dk.data_dictionary["prediction_features"], device=self.device ) - logger.info(self.model.model) - logger.info(self.model.model) - y = self.model.model(x) pred_df = DataFrame(y.detach().numpy(), columns=[dk.label_list[0]]) return (pred_df, dk.do_predict) From bc9454e0f9a2a67e1b9ffdb6ade4fe500aee7682 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 3 Apr 2023 16:36:38 +0300 Subject: [PATCH 135/208] add device to data convertor class doc --- freqtrade/freqai/torch/PyTorchDataConvertor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchDataConvertor.py b/freqtrade/freqai/torch/PyTorchDataConvertor.py index 1c948c72e..1070b0fb5 100644 --- a/freqtrade/freqai/torch/PyTorchDataConvertor.py +++ b/freqtrade/freqai/torch/PyTorchDataConvertor.py @@ -11,7 +11,7 @@ class PyTorchDataConvertor(ABC): def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: """ :param df: "*_features" dataframe. - :param device: cpu/gpu. + :param device: The device to use for training (e.g. 'cpu', 'cuda'). :returns: tuple of tensors. """ @@ -19,7 +19,7 @@ class PyTorchDataConvertor(ABC): def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: """ :param df: "*_labels" dataframe. - :param device: cpu/gpu. + :param device: The device to use for training (e.g. 'cpu', 'cuda'). :returns: tuple of tensors. """ From 7b494c8333f5d0bdbec0d93376339b79d1f07909 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 3 Apr 2023 16:39:49 +0300 Subject: [PATCH 136/208] add documentation to pytorch data convertor --- freqtrade/freqai/torch/PyTorchDataConvertor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/freqtrade/freqai/torch/PyTorchDataConvertor.py b/freqtrade/freqai/torch/PyTorchDataConvertor.py index 1070b0fb5..5982a1b48 100644 --- a/freqtrade/freqai/torch/PyTorchDataConvertor.py +++ b/freqtrade/freqai/torch/PyTorchDataConvertor.py @@ -31,6 +31,12 @@ class DefaultPyTorchDataConvertor(PyTorchDataConvertor): target_tensor_type: Optional[torch.dtype] = None, squeeze_target_tensor: bool = False ): + """ + :param target_tensor_type: type of target tensor, for classification use + torch.long, for regressor use torch.float or torch.double. + :param squeeze_target_tensor: controls the target shape, used for loss functions + that requires 0D or 1D. + """ self._target_tensor_type = target_tensor_type self._squeeze_target_tensor = squeeze_target_tensor From d9d99931792d0cf21c1e83f2b75da330f3ea8bdf Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 3 Apr 2023 17:06:39 +0300 Subject: [PATCH 137/208] add documentation --- freqtrade/freqai/base_models/BasePyTorchModel.py | 4 ++++ freqtrade/freqai/torch/PyTorchDataConvertor.py | 7 +++++++ freqtrade/freqai/torch/PyTorchModelTrainer.py | 1 + 3 files changed, 12 insertions(+) diff --git a/freqtrade/freqai/base_models/BasePyTorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py index d017f1fec..8177b8eb8 100644 --- a/freqtrade/freqai/base_models/BasePyTorchModel.py +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -76,4 +76,8 @@ class BasePyTorchModel(IFreqaiModel, ABC): @property @abstractmethod def data_convertor(self) -> PyTorchDataConvertor: + """ + a class responsible for converting `*_features` & `*_labels` pandas dataframes + to pytorch tensors. + """ raise NotImplementedError("Abstract property") diff --git a/freqtrade/freqai/torch/PyTorchDataConvertor.py b/freqtrade/freqai/torch/PyTorchDataConvertor.py index 5982a1b48..e7d5c3ffe 100644 --- a/freqtrade/freqai/torch/PyTorchDataConvertor.py +++ b/freqtrade/freqai/torch/PyTorchDataConvertor.py @@ -6,6 +6,10 @@ import torch class PyTorchDataConvertor(ABC): + """ + This class is responsible for converting `*_features` & `*_labels` pandas dataframes + to pytorch tensors. + """ @abstractmethod def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: @@ -25,6 +29,9 @@ class PyTorchDataConvertor(ABC): class DefaultPyTorchDataConvertor(PyTorchDataConvertor): + """ + A default conversion that keeps features dataframe shapes. + """ def __init__( self, diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index 09de6f940..6449d98b5 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -36,6 +36,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): :param init_model: A dictionary containing the initial model/optimizer state_dict and model_meta_data saved by self.save() method. :param model_meta_data: Additional metadata about the model (optional). + :param data_convertor: convertor from pd.DataFrame to torch.tensor. :param max_iters: The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs. From 0c4574b3b7e2dac0ec2d441e2bc8bccf4c3ff9f5 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 3 Apr 2023 18:10:47 +0300 Subject: [PATCH 138/208] prevent mypy error, explicitly unpack input list of pytorch mlp model, --- freqtrade/freqai/torch/PyTorchMLPModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/torch/PyTorchMLPModel.py b/freqtrade/freqai/torch/PyTorchMLPModel.py index 01192e115..192d2ad89 100644 --- a/freqtrade/freqai/torch/PyTorchMLPModel.py +++ b/freqtrade/freqai/torch/PyTorchMLPModel.py @@ -48,7 +48,7 @@ class PyTorchMLPModel(nn.Module): self.dropout = nn.Dropout(p=dropout_percent) def forward(self, x: List[torch.Tensor]) -> torch.Tensor: - x, = x + x = x[0] x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.blocks(x) From 6b204c97ed44c4e1c8258a3cebfda8ea2694f44d Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 3 Apr 2023 19:02:07 +0300 Subject: [PATCH 139/208] fix pytorch data convertor type hints --- freqtrade/freqai/torch/PyTorchDataConvertor.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchDataConvertor.py b/freqtrade/freqai/torch/PyTorchDataConvertor.py index e7d5c3ffe..a31ccdc79 100644 --- a/freqtrade/freqai/torch/PyTorchDataConvertor.py +++ b/freqtrade/freqai/torch/PyTorchDataConvertor.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Optional, Tuple +from typing import List, Optional import pandas as pd import torch @@ -12,19 +12,17 @@ class PyTorchDataConvertor(ABC): """ @abstractmethod - def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: + def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]: """ :param df: "*_features" dataframe. :param device: The device to use for training (e.g. 'cpu', 'cuda'). - :returns: tuple of tensors. """ @abstractmethod - def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: + def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]: """ :param df: "*_labels" dataframe. :param device: The device to use for training (e.g. 'cpu', 'cuda'). - :returns: tuple of tensors. """ @@ -47,14 +45,14 @@ class DefaultPyTorchDataConvertor(PyTorchDataConvertor): self._target_tensor_type = target_tensor_type self._squeeze_target_tensor = squeeze_target_tensor - def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: + def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]: x = torch.from_numpy(df.values).float() if device: x = x.to(device) - return x, + return [x] - def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: + def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]: y = torch.from_numpy(df.values) if self._target_tensor_type: @@ -66,4 +64,4 @@ class DefaultPyTorchDataConvertor(PyTorchDataConvertor): if device: y = y.to(device) - return y, + return [y] From 92a060c5b45f683009186c6a7169e5f8bfae9b55 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Apr 2023 20:18:57 +0200 Subject: [PATCH 140/208] Make stop_price_parameter configurable by exchange --- freqtrade/exchange/exchange.py | 13 +++++++------ freqtrade/exchange/okx.py | 20 ++------------------ 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 6c236106f..4d7be8fdf 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -60,6 +60,7 @@ class Exchange: # or by specifying them in the configuration. _ft_has_default: Dict = { "stoploss_on_exchange": False, + "stop_price_param": "stopPrice", "order_time_in_force": ["GTC"], "ohlcv_params": {}, "ohlcv_candle_limit": 500, @@ -1115,11 +1116,11 @@ class Exchange: """ if not self._ft_has.get('stoploss_on_exchange'): raise OperationalException(f"stoploss is not implemented for {self.name}.") - + price_param = self._ft_has['stop_price_param'] return ( - order.get('stopPrice', None) is None - or ((side == "sell" and stop_loss > float(order['stopPrice'])) or - (side == "buy" and stop_loss < float(order['stopPrice']))) + order.get(price_param, None) is None + or ((side == "sell" and stop_loss > float(order[price_param])) or + (side == "buy" and stop_loss < float(order[price_param]))) ) def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]: @@ -1159,8 +1160,8 @@ class Exchange: def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict: params = self._params.copy() - # Verify if stopPrice works for your exchange! - params.update({'stopPrice': stop_price}) + # Verify if stopPrice works for your exchange, else configure stop_price_param + params.update({self._ft_has['stop_price_param']: stop_price}) return params @retrier(retries=0) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index a4fcaeca0..84b7deb7a 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -28,6 +28,7 @@ class Okx(Exchange): "funding_fee_timeframe": "8h", "stoploss_order_types": {"limit": "limit"}, "stoploss_on_exchange": True, + "stop_price_param": "stopLossPrice", } _ft_has_futures: Dict = { "tickers_have_quoteVolume": False, @@ -162,29 +163,12 @@ class Okx(Exchange): return pair_tiers[-1]['maxNotional'] / leverage def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict: - - params = self._params.copy() - # Verify if stopPrice works for your exchange! - params.update({'stopLossPrice': stop_price}) - + params = super()._get_stop_params(side, ordertype, stop_price) if self.trading_mode == TradingMode.FUTURES and self.margin_mode: params['tdMode'] = self.margin_mode.value params['posSide'] = self._get_posSide(side, True) return params - def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: - """ - OKX uses non-default stoploss price naming. - """ - if not self._ft_has.get('stoploss_on_exchange'): - raise OperationalException(f"stoploss is not implemented for {self.name}.") - - return ( - order.get('stopLossPrice', None) is None - or ((side == "sell" and stop_loss > float(order['stopLossPrice'])) or - (side == "buy" and stop_loss < float(order['stopLossPrice']))) - ) - def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: if self._config['dry_run']: return self.fetch_dry_run_order(order_id) From fe02f611fbc82edcde6ac743b243cd189f0075dc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Apr 2023 06:46:35 +0200 Subject: [PATCH 141/208] Fix typo in reinforcement learning closes #8431 --- docs/freqai-reinforcement-learning.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/freqai-reinforcement-learning.md b/docs/freqai-reinforcement-learning.md index f5679a4ba..f298dbf4d 100644 --- a/docs/freqai-reinforcement-learning.md +++ b/docs/freqai-reinforcement-learning.md @@ -180,7 +180,7 @@ As you begin to modify the strategy and the prediction model, you will quickly r # you can use feature values from dataframe # Assumes the shifted RSI indicator has been generated in the strategy. - rsi_now = self.raw_features[f"%-rsi-period-10_shift-1_{pair}_" + rsi_now = self.raw_features[f"%-rsi-period_10_shift-1_{pair}_" f"{self.config['timeframe']}"].iloc[self._current_tick] # reward agent for entering trades From 26738370c75ceaeafcc9c4c353f02a30d7dedd19 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 4 Apr 2023 12:12:02 +0300 Subject: [PATCH 142/208] pytorch mlp add explicit annotation to fix mypy error --- freqtrade/freqai/torch/PyTorchMLPModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/torch/PyTorchMLPModel.py b/freqtrade/freqai/torch/PyTorchMLPModel.py index 192d2ad89..94cfa8e64 100644 --- a/freqtrade/freqai/torch/PyTorchMLPModel.py +++ b/freqtrade/freqai/torch/PyTorchMLPModel.py @@ -48,7 +48,7 @@ class PyTorchMLPModel(nn.Module): self.dropout = nn.Dropout(p=dropout_percent) def forward(self, x: List[torch.Tensor]) -> torch.Tensor: - x = x[0] + x: torch.Tensor = x[0] x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.blocks(x) From a6555242218ddbf7269353baad69e0472531b61d Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 4 Apr 2023 12:24:29 +0300 Subject: [PATCH 143/208] pytorch mlp rename input to fix mypy error --- freqtrade/freqai/torch/PyTorchMLPModel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchMLPModel.py b/freqtrade/freqai/torch/PyTorchMLPModel.py index 94cfa8e64..e50ccd5ef 100644 --- a/freqtrade/freqai/torch/PyTorchMLPModel.py +++ b/freqtrade/freqai/torch/PyTorchMLPModel.py @@ -47,8 +47,8 @@ class PyTorchMLPModel(nn.Module): self.relu = nn.ReLU() self.dropout = nn.Dropout(p=dropout_percent) - def forward(self, x: List[torch.Tensor]) -> torch.Tensor: - x: torch.Tensor = x[0] + def forward(self, tensors: List[torch.Tensor]) -> torch.Tensor: + x: torch.Tensor = tensors[0] x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.blocks(x) From f03a99918a422ea5a513bb3ad65a71a9b3b10eb8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Apr 2023 20:04:28 +0200 Subject: [PATCH 144/208] Ensure hyper param file can be loaded closes #8452 --- freqtrade/optimize/hyperopt_tools.py | 15 +++++++++++++-- freqtrade/strategy/hyper.py | 5 ++--- tests/strategy/test_interface.py | 9 ++++++--- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index e2133a956..1e7befdf6 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -23,6 +23,8 @@ logger = logging.getLogger(__name__) NON_OPT_PARAM_APPENDIX = " # value loaded from strategy" +HYPER_PARAMS_FILE_FORMAT = rapidjson.NM_NATIVE | rapidjson.NM_NAN + def hyperopt_serializer(x): if isinstance(x, np.integer): @@ -76,9 +78,18 @@ class HyperoptTools(): with filename.open('w') as f: rapidjson.dump(final_params, f, indent=2, default=hyperopt_serializer, - number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN + number_mode=HYPER_PARAMS_FILE_FORMAT ) + @staticmethod + def load_params(filename: Path) -> Dict: + """ + Load parameters from file + """ + with filename.open('r') as f: + params = rapidjson.load(f, number_mode=HYPER_PARAMS_FILE_FORMAT) + return params + @staticmethod def try_export_params(config: Config, strategy_name: str, params: Dict): if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get('disableparamexport', False): @@ -189,7 +200,7 @@ class HyperoptTools(): for s in ['buy', 'sell', 'protection', 'roi', 'stoploss', 'trailing', 'max_open_trades']: HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s) - print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) + print(rapidjson.dumps(result_dict, default=str, number_mode=HYPER_PARAMS_FILE_FORMAT)) else: HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:", diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 52ba22951..d38110a2a 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -8,7 +8,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union from freqtrade.constants import Config from freqtrade.exceptions import OperationalException -from freqtrade.misc import deep_merge_dicts, json_load +from freqtrade.misc import deep_merge_dicts from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.strategy.parameters import BaseParameter @@ -124,8 +124,7 @@ class HyperStrategyMixin: if filename.is_file(): logger.info(f"Loading parameters from file {filename}") try: - with filename.open('r') as f: - params = json_load(f) + params = HyperoptTools.load_params(filename) if params.get('strategy_name') != self.__class__.__name__: raise OperationalException('Invalid parameter file provided.') return params diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 7b1399507..713ffa5cb 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -986,7 +986,8 @@ def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog): } } } - mocker.patch('freqtrade.strategy.hyper.json_load', return_value=expected_result) + mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params', + return_value=expected_result) PairLocks.timeframe = default_conf['timeframe'] strategy = StrategyResolver.load_strategy(default_conf) assert strategy.stoploss == -0.05 @@ -1005,11 +1006,13 @@ def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog): } } - mocker.patch('freqtrade.strategy.hyper.json_load', return_value=expected_result) + mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params', + return_value=expected_result) with pytest.raises(OperationalException, match="Invalid parameter file provided."): StrategyResolver.load_strategy(default_conf) - mocker.patch('freqtrade.strategy.hyper.json_load', MagicMock(side_effect=ValueError())) + mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params', + MagicMock(side_effect=ValueError())) StrategyResolver.load_strategy(default_conf) assert log_has("Invalid parameter file format.", caplog) From dae3f72be73a77a4ef3f0585a54ea41013b5f762 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 Apr 2023 14:11:31 +0200 Subject: [PATCH 145/208] Bump Dockerfile to latest 3.10 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6a4a168c1..655f9ee94 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10.10-slim-bullseye as base +FROM python:3.10.11-slim-bullseye as base # Setup env ENV LANG C.UTF-8 From a75d8910076990fbf8ff6245170c8f4b8a30a7f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 Apr 2023 14:45:06 +0200 Subject: [PATCH 146/208] Ensure minimum sqlalchemy version is respected --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index edd7b243b..131e8a8a7 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ setup( install_requires=[ # from requirements.txt 'ccxt>=2.6.26', - 'SQLAlchemy', + 'SQLAlchemy>=2.0.6', 'python-telegram-bot>=13.4', 'arrow>=0.17.0', 'cachetools', From 77985fa59144372eec901fa727730d11f64bab5e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 Apr 2023 14:49:53 +0200 Subject: [PATCH 147/208] Update thread name for uvicorn worker --- freqtrade/rpc/api_server/uvicorn_threaded.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/uvicorn_threaded.py b/freqtrade/rpc/api_server/uvicorn_threaded.py index a79c1a5fc..48786bec2 100644 --- a/freqtrade/rpc/api_server/uvicorn_threaded.py +++ b/freqtrade/rpc/api_server/uvicorn_threaded.py @@ -55,7 +55,7 @@ class UvicornServer(uvicorn.Server): @contextlib.contextmanager def run_in_thread(self): - self.thread = threading.Thread(target=self.run) + self.thread = threading.Thread(target=self.run, name='FTUvicorn') self.thread.start() while not self.started: time.sleep(1e-3) From 1952e453bbdb7ea2076a0cc3285adb428b3fdb62 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 Apr 2023 17:35:11 +0200 Subject: [PATCH 148/208] Improved formatting for fetch order_or_stop calls --- freqtrade/freqtradebot.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index af4f42feb..26e918f18 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1483,8 +1483,8 @@ class FreqtradeBot(LoggingMixin): return False try: - order = self.exchange.cancel_order_with_result(order['id'], trade.pair, - trade.amount) + order = self.exchange.cancel_order_with_result( + order['id'], trade.pair, trade.amount) except InvalidOrderException: logger.exception( f"Could not cancel {trade.exit_side} order {trade.open_order_id}") @@ -1786,9 +1786,8 @@ class FreqtradeBot(LoggingMixin): if not stoploss_order: logger.info(f'Found open order for {trade}') try: - order = action_order or self.exchange.fetch_order_or_stoploss_order(order_id, - trade.pair, - stoploss_order) + order = action_order or self.exchange.fetch_order_or_stoploss_order( + order_id, trade.pair, stoploss_order) except InvalidOrderException as exception: logger.warning('Unable to fetch order %s: %s', order_id, exception) return False From f8d89c46e55d733393554e349b0980e0cc8ad2b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 Apr 2023 19:48:44 +0200 Subject: [PATCH 149/208] Don't reset open_order_id if the order didn't cancel --- freqtrade/freqtradebot.py | 3 ++- tests/test_freqtradebot.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 26e918f18..48e3ec209 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1496,17 +1496,18 @@ class FreqtradeBot(LoggingMixin): # Order might be filled above in odd timing issues. if order.get('status') in ('canceled', 'cancelled'): trade.exit_reason = None + trade.open_order_id = None else: trade.exit_reason = exit_reason_prev cancelled = True else: reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] trade.exit_reason = None + trade.open_order_id = None self.update_trade_state(trade, trade.open_order_id, order) logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') - trade.open_order_id = None trade.close_rate = None trade.close_rate_requested = None diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ab5dd4af5..7bded0f82 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2955,6 +2955,9 @@ def test_manage_open_orders_exit_usercustom( assert rpc_mock.call_count == 2 assert freqtrade.strategy.check_exit_timeout.call_count == 1 assert freqtrade.strategy.check_entry_timeout.call_count == 0 + trade = Trade.session.scalars(select(Trade)).first() + # cancelling didn't succeed - order-id remains open. + assert trade.open_order_id is not None # 2nd canceled trade - Fail execute exit caplog.clear() @@ -3465,6 +3468,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None: # TODO: should not be magicmock trade = MagicMock() + trade.open_order_id = '125' reason = CANCEL_REASON['TIMEOUT'] order = {'remaining': 1, 'id': '125', @@ -3472,6 +3476,10 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None: 'status': "open"} assert not freqtrade.handle_cancel_exit(trade, order, reason) + # mocker.patch(f'{EXMS}.cancel_order_with_result', return_value=order) + # assert not freqtrade.handle_cancel_exit(trade, order, reason) + # assert trade.open_order_id == '125' + @pytest.mark.parametrize("is_short, open_rate, amt", [ (False, 2.0, 30.0), From c083723698e4894a92109ffc8977a83987ecc3cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 10:02:38 +0200 Subject: [PATCH 150/208] Add initial version of key value store --- freqtrade/persistence/__init__.py | 1 + freqtrade/persistence/key_value_store.py | 97 ++++++++++++++++++++++++ freqtrade/persistence/models.py | 2 + 3 files changed, 100 insertions(+) create mode 100644 freqtrade/persistence/key_value_store.py diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index 9e1a7e922..c69a54b7f 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa: F401 +from freqtrade.persistence.key_value_store import KeyValueStore from freqtrade.persistence.models import init_db from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.trade_model import LocalTrade, Order, Trade diff --git a/freqtrade/persistence/key_value_store.py b/freqtrade/persistence/key_value_store.py new file mode 100644 index 000000000..5ad98d69d --- /dev/null +++ b/freqtrade/persistence/key_value_store.py @@ -0,0 +1,97 @@ +from datetime import datetime, timezone +from enum import Enum +from typing import ClassVar, Optional, Union + +from sqlalchemy import String +from sqlalchemy.orm import Mapped, mapped_column + +from freqtrade.persistence.base import ModelBase, SessionType + + +ValueTypes = Union[str, datetime, float, int] + + +class ValueTypesEnum(str, Enum): + STRING = 'str' + DATETIME = 'datetime' + FLOAT = 'float' + INT = 'int' + + +class _KeyValueStoreModel(ModelBase): + """ + Pair Locks database model. + """ + __tablename__ = 'KeyValueStore' + session: ClassVar[SessionType] + + id: Mapped[int] = mapped_column(primary_key=True) + + key: Mapped[str] = mapped_column(String(25), nullable=False, index=True) + + value_type: Mapped[ValueTypesEnum] = mapped_column(String(25), nullable=False) + + string_value: Mapped[Optional[str]] + datetime_value: Mapped[Optional[datetime]] + float_value: Mapped[Optional[float]] + int_value: Mapped[Optional[int]] + + +class KeyValueStore(): + + @staticmethod + def get_value(key: str) -> Optional[ValueTypes]: + """ + Get the value for the given key. + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key).first() + if kv is None: + return None + if kv.value_type == ValueTypesEnum.STRING: + return kv.string_value + if kv.value_type == ValueTypesEnum.DATETIME and kv.datetime_value is not None: + return kv.datetime_value.replace(tzinfo=timezone.utc) + if kv.value_type == ValueTypesEnum.FLOAT: + return kv.float_value + if kv.value_type == ValueTypesEnum.INT: + return kv.int_value + # This should never happen unless someone messed with the database manually + raise ValueError(f'Unknown value type {kv.value_type}') # pragma: no cover + + @staticmethod + def store_value(key: str, value: ValueTypes) -> None: + """ + Store the given value for the given key. + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key).first() + if kv is None: + kv = _KeyValueStoreModel(key=key) + if isinstance(value, str): + kv.value_type = ValueTypesEnum.STRING + kv.string_value = value + elif isinstance(value, datetime): + kv.value_type = ValueTypesEnum.DATETIME + kv.datetime_value = value + elif isinstance(value, float): + kv.value_type = ValueTypesEnum.FLOAT + kv.float_value = value + elif isinstance(value, int): + kv.value_type = ValueTypesEnum.INT + kv.int_value = value + else: + raise ValueError(f'Unknown value type {kv.value_type}') + _KeyValueStoreModel.session.add(kv) + _KeyValueStoreModel.session.commit() + + @staticmethod + def delete_value(key: str) -> None: + """ + Delete the value for the given key. + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key).first() + if kv is not None: + _KeyValueStoreModel.session.delete(kv) + _KeyValueStoreModel.session.commit() diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 2315c0acc..e561e727b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -13,6 +13,7 @@ from sqlalchemy.pool import StaticPool from freqtrade.exceptions import OperationalException from freqtrade.persistence.base import ModelBase +from freqtrade.persistence.key_value_store import _KeyValueStoreModel from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.pairlock import PairLock from freqtrade.persistence.trade_model import Order, Trade @@ -76,6 +77,7 @@ def init_db(db_url: str) -> None: bind=engine, autoflush=False), scopefunc=get_request_or_thread_id) Order.session = Trade.session PairLock.session = Trade.session + _KeyValueStoreModel.session = Trade.session previous_tables = inspect(engine).get_table_names() ModelBase.metadata.create_all(engine) From 4d4f4bf23ea0fa8aff1888b1fa41ca22ee0adec6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 10:07:21 +0200 Subject: [PATCH 151/208] Add test for key_value_store --- tests/persistence/test_key_value_store.py | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/persistence/test_key_value_store.py diff --git a/tests/persistence/test_key_value_store.py b/tests/persistence/test_key_value_store.py new file mode 100644 index 000000000..27e56ba11 --- /dev/null +++ b/tests/persistence/test_key_value_store.py @@ -0,0 +1,37 @@ +from datetime import datetime, timedelta, timezone + +import pytest + +from freqtrade.persistence.key_value_store import KeyValueStore + + +@pytest.mark.usefixtures("init_persistence") +def test_key_value_store(time_machine): + start = datetime(2023, 1, 1, 4, tzinfo=timezone.utc) + time_machine.move_to(start, tick=False) + + KeyValueStore.store_value("test", "testStringValue") + KeyValueStore.store_value("test_dt", datetime.now(timezone.utc)) + KeyValueStore.store_value("test_float", 22.51) + KeyValueStore.store_value("test_int", 15) + + assert KeyValueStore.get_value("test") == "testStringValue" + assert KeyValueStore.get_value("test_dt") == datetime.now(timezone.utc) + assert KeyValueStore.get_value("test_float") == 22.51 + assert KeyValueStore.get_value("test_int") == 15 + + time_machine.move_to(start + timedelta(days=20, hours=5), tick=False) + assert KeyValueStore.get_value("test_dt") != datetime.now(timezone.utc) + assert KeyValueStore.get_value("test_dt") == start + # Test update works + KeyValueStore.store_value("test_dt", datetime.now(timezone.utc)) + assert KeyValueStore.get_value("test_dt") == datetime.now(timezone.utc) + + KeyValueStore.store_value("test_float", 23.51) + assert KeyValueStore.get_value("test_float") == 23.51 + # test deleting + KeyValueStore.delete_value("test_float") + assert KeyValueStore.get_value("test_float") is None + + with pytest.raises(ValueError, match=r"Unknown value type"): + KeyValueStore.store_value("test_float", {'some': 'dict'}) From ac817b7808ddf0559d2bb7d8142cdade5434af11 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 10:09:31 +0200 Subject: [PATCH 152/208] Improve docstrings for key-value store --- freqtrade/persistence/key_value_store.py | 9 +++++++++ tests/persistence/test_key_value_store.py | 2 ++ 2 files changed, 11 insertions(+) diff --git a/freqtrade/persistence/key_value_store.py b/freqtrade/persistence/key_value_store.py index 5ad98d69d..109f94fcc 100644 --- a/freqtrade/persistence/key_value_store.py +++ b/freqtrade/persistence/key_value_store.py @@ -38,11 +38,17 @@ class _KeyValueStoreModel(ModelBase): class KeyValueStore(): + """ + Generic bot-wide, persistent key-value store + Can be used to store generic values, e.g. very first bot startup time. + Supports the types str, datetime, float and int. + """ @staticmethod def get_value(key: str) -> Optional[ValueTypes]: """ Get the value for the given key. + :param key: Key to get the value for """ kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( _KeyValueStoreModel.key == key).first() @@ -63,6 +69,8 @@ class KeyValueStore(): def store_value(key: str, value: ValueTypes) -> None: """ Store the given value for the given key. + :param key: Key to store the value for - can be used in get-value to retrieve the key + :param value: Value to store - can be str, datetime, float or int """ kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( _KeyValueStoreModel.key == key).first() @@ -89,6 +97,7 @@ class KeyValueStore(): def delete_value(key: str) -> None: """ Delete the value for the given key. + :param key: Key to delete the value for """ kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( _KeyValueStoreModel.key == key).first() diff --git a/tests/persistence/test_key_value_store.py b/tests/persistence/test_key_value_store.py index 27e56ba11..8da5e4659 100644 --- a/tests/persistence/test_key_value_store.py +++ b/tests/persistence/test_key_value_store.py @@ -32,6 +32,8 @@ def test_key_value_store(time_machine): # test deleting KeyValueStore.delete_value("test_float") assert KeyValueStore.get_value("test_float") is None + # Delete same value again (should not fail) + KeyValueStore.delete_value("test_float") with pytest.raises(ValueError, match=r"Unknown value type"): KeyValueStore.store_value("test_float", {'some': 'dict'}) From 48d3c8e62e8988376c472dd985797c06ecfed944 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sat, 8 Apr 2023 12:09:53 +0200 Subject: [PATCH 153/208] fix model loading from disk bug, improve doc, clarify installation/docker instructions, add a torch tag to the freqairl docker image. Fix seriously outdated prediction_model docstrings --- build_helpers/publish_docker_multi.sh | 2 + docs/freqai-configuration.md | 62 +++++++++++++++++-- freqtrade/freqai/data_drawer.py | 4 +- .../prediction_models/CatboostClassifier.py | 14 +++-- .../CatboostClassifierMultiTarget.py | 14 +++-- .../prediction_models/CatboostRegressor.py | 14 +++-- .../CatboostRegressorMultiTarget.py | 14 +++-- .../prediction_models/LightGBMClassifier.py | 14 +++-- .../LightGBMClassifierMultiTarget.py | 14 +++-- .../prediction_models/LightGBMRegressor.py | 18 +++--- .../LightGBMRegressorMultiTarget.py | 14 +++-- .../prediction_models/PyTorchMLPClassifier.py | 5 +- .../prediction_models/PyTorchMLPRegressor.py | 5 +- .../prediction_models/XGBoostClassifier.py | 14 +++-- .../prediction_models/XGBoostRFClassifier.py | 14 +++-- .../prediction_models/XGBoostRFRegressor.py | 14 +++-- .../prediction_models/XGBoostRegressor.py | 14 +++-- .../XGBoostRegressorMultiTarget.py | 14 +++-- freqtrade/freqai/torch/PyTorchModelTrainer.py | 10 ++- .../freqai/torch/PyTorchTrainerInterface.py | 2 +- setup.sh | 2 +- 21 files changed, 195 insertions(+), 83 deletions(-) diff --git a/build_helpers/publish_docker_multi.sh b/build_helpers/publish_docker_multi.sh index 3e5e61564..6c5d11d94 100755 --- a/build_helpers/publish_docker_multi.sh +++ b/build_helpers/publish_docker_multi.sh @@ -7,6 +7,7 @@ TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG_PLOT=${TAG}_plot TAG_FREQAI=${TAG}_freqai TAG_FREQAI_RL=${TAG_FREQAI}rl +TAG_FREQAI_RL=${TAG_FREQAI}torch TAG_PI="${TAG}_pi" PI_PLATFORM="linux/arm/v7" @@ -64,6 +65,7 @@ docker build --cache-from freqtrade:${TAG_FREQAI} --build-arg sourceimage=${CACH docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI docker tag freqtrade:$TAG_FREQAI_RL ${CACHE_IMAGE}:$TAG_FREQAI_RL +docker tag freqtrade:$TAG_FREQAI_RL ${CACHE_IMAGE}:$TAG_FREQAI_TORCH # Run backtest docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3 diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 442705b53..8f1aa5079 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -237,7 +237,7 @@ df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'do df['&s-up_or_down'] = np.where( df["close"].shift(-100) == df["close"], 'same', df['&s-up_or_down']) ``` -## PyTorch Models +## PyTorch Module ### Quick start @@ -247,14 +247,16 @@ The easiest way to quickly run a pytorch model is with the following command (fo freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel PyTorchMLPRegressor --strategy-path freqtrade/templates ``` +!!! note "Installation/docker" + The PyTorch module requires large packages such as `torch`, which should be explicitly requested during `./setup.sh -i` by answering "y" to the question "Do you also want dependencies for freqai-rl or PyTorch (~700mb additional space required) [y/N]?". + Users who prefer docker should ensure they use the docker image appended with `_freqaitorch`. + ### Structure #### Model -You can use any pytorch model. Here is an example of logistic regression model implementation using pytorch (should be used with nn.BCELoss criterion) for classification tasks. +You can construct your own Neural Network architecture in PyTorch by simply defining your `nn.Module` class inside your custom [`IFreqaiModel` file](#using-different-prediction-models) and then using that class in your `def train()` function. Here is an example of logistic regression model implementation using PyTorch (should be used with nn.BCELoss criterion) for classification tasks. ```python -import torch.nn as nn -import torch class LogisticRegression(nn.Module): def __init__(self, input_size: int): @@ -268,11 +270,59 @@ class LogisticRegression(nn.Module): out = self.linear(x) out = self.activation(out) return out + +class MyCoolPyTorchClassifier(BasePyTorchClassifier): + """ + This is a custom IFreqaiModel showing how a user might setup their own + custom Neural Network architecture for their training. + """ + + @property + def data_convertor(self) -> PyTorchDataConvertor: + return DefaultPyTorchDataConvertor(target_tensor_type=torch.float) + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model + """ + + class_names = self.get_class_names() + self.convert_label_column_to_int(data_dictionary, dk, class_names) + n_features = data_dictionary["train_features"].shape[-1] + model = LogisticRegression( + input_dim=n_features + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.CrossEntropyLoss() + init_model = self.get_init_model(dk.pair) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + model_meta_data={"class_names": class_names}, + device=self.device, + init_model=init_model, + data_convertor=self.data_convertor, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary, self.splits) + return trainer + ``` - #### Trainer -The `PyTorchModelTrainer` performs the idiomatic pytorch train loop: +The `PyTorchModelTrainer` performs the idiomatic PyTorch train loop: Define our model, loss function, and optimizer, and then move them to the appropriate device (GPU or CPU). Inside the loop, we iterate through the batches in the dataloader, move the data to the device, compute the prediction and loss, backpropagate, and update the model parameters using the optimizer. In addition, the trainer is responsible for the following: diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index c8dadb171..b68a9dcad 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -539,7 +539,9 @@ class FreqaiDataDrawer: model = MODELCLASS.load(dk.data_path / f"{dk.model_filename}_model") elif self.model_type == 'pytorch': import torch - model = torch.load(dk.data_path / f"{dk.model_filename}_model.zip") + zip = torch.load(dk.data_path / f"{dk.model_filename}_model.zip") + model = zip["pytrainer"] + model = model.load_from_checkpoint(zip) if Path(dk.data_path / f"{dk.model_filename}_svm_model.joblib").is_file(): dk.svm_model = load(dk.data_path / f"{dk.model_filename}_svm_model.joblib") diff --git a/freqtrade/freqai/prediction_models/CatboostClassifier.py b/freqtrade/freqai/prediction_models/CatboostClassifier.py index ca1d8ece0..b9904e40d 100644 --- a/freqtrade/freqai/prediction_models/CatboostClassifier.py +++ b/freqtrade/freqai/prediction_models/CatboostClassifier.py @@ -14,16 +14,20 @@ logger = logging.getLogger(__name__) class CatboostClassifier(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ train_data = Pool( diff --git a/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py index c6f900fad..58c47566a 100644 --- a/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py @@ -15,16 +15,20 @@ logger = logging.getLogger(__name__) class CatboostClassifierMultiTarget(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ cbc = CatBoostClassifier( diff --git a/freqtrade/freqai/prediction_models/CatboostRegressor.py b/freqtrade/freqai/prediction_models/CatboostRegressor.py index 4b17a703b..28b1b11cc 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressor.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressor.py @@ -14,16 +14,20 @@ logger = logging.getLogger(__name__) class CatboostRegressor(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ train_data = Pool( diff --git a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py index 976d0b29b..1562c2024 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py @@ -15,16 +15,20 @@ logger = logging.getLogger(__name__) class CatboostRegressorMultiTarget(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ cbr = CatBoostRegressor( diff --git a/freqtrade/freqai/prediction_models/LightGBMClassifier.py b/freqtrade/freqai/prediction_models/LightGBMClassifier.py index e467ad3c1..45f3a31d0 100644 --- a/freqtrade/freqai/prediction_models/LightGBMClassifier.py +++ b/freqtrade/freqai/prediction_models/LightGBMClassifier.py @@ -12,16 +12,20 @@ logger = logging.getLogger(__name__) class LightGBMClassifier(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) == 0: diff --git a/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py index d1eb6daa2..72a8ee259 100644 --- a/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py @@ -13,16 +13,20 @@ logger = logging.getLogger(__name__) class LightGBMClassifierMultiTarget(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ lgb = LGBMClassifier(**self.model_training_parameters) diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressor.py b/freqtrade/freqai/prediction_models/LightGBMRegressor.py index 85c9b691c..3d1c30ed3 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressor.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressor.py @@ -12,18 +12,20 @@ logger = logging.getLogger(__name__) class LightGBMRegressor(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ - Most regressors use the same function names and arguments e.g. user - can drop in LGBMRegressor in place of CatBoostRegressor and all data - management will be properly handled by Freqai. - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) == 0: diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py index 37c6bb186..663a611f0 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py @@ -13,16 +13,20 @@ logger = logging.getLogger(__name__) class LightGBMRegressorMultiTarget(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ lgb = LGBMRegressor(**self.model_training_parameters) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 8694453be..ea7981405 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -57,8 +57,9 @@ class PyTorchMLPClassifier(BasePyTorchClassifier): def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model :raises ValueError: If self.class_names is not defined in the parent class. """ diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 5ca3486e1..64f0f4b03 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -55,8 +55,9 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ n_features = data_dictionary["train_features"].shape[-1] diff --git a/freqtrade/freqai/prediction_models/XGBoostClassifier.py b/freqtrade/freqai/prediction_models/XGBoostClassifier.py index 67c7c7783..b6f04b497 100644 --- a/freqtrade/freqai/prediction_models/XGBoostClassifier.py +++ b/freqtrade/freqai/prediction_models/XGBoostClassifier.py @@ -18,16 +18,20 @@ logger = logging.getLogger(__name__) class XGBoostClassifier(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ X = data_dictionary["train_features"].to_numpy() diff --git a/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py b/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py index 470c283ea..20156e9fd 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py +++ b/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py @@ -18,16 +18,20 @@ logger = logging.getLogger(__name__) class XGBoostRFClassifier(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ X = data_dictionary["train_features"].to_numpy() diff --git a/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py index e7cc27f2e..1aefbf19a 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py +++ b/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py @@ -12,16 +12,20 @@ logger = logging.getLogger(__name__) class XGBoostRFRegressor(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ X = data_dictionary["train_features"] diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRegressor.py index 9a280286b..93dfb319e 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressor.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressor.py @@ -12,16 +12,20 @@ logger = logging.getLogger(__name__) class XGBoostRegressor(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ X = data_dictionary["train_features"] diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py index 920745ec9..a0330485e 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py @@ -13,16 +13,20 @@ logger = logging.getLogger(__name__) class XGBoostRegressorMultiTarget(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ xgb = XGBRegressor(**self.model_training_parameters) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index 6449d98b5..9c1a1cb6e 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -5,7 +5,7 @@ from typing import Any, Dict, List, Optional import pandas as pd import torch -import torch.nn as nn +from torch import nn from torch.optim import Optimizer from torch.utils.data import DataLoader, TensorDataset @@ -169,6 +169,12 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): n_batches = math.ceil(n_obs // batch_size) epochs = math.ceil(n_iters // n_batches) + if epochs <= 10: + logger.warning("User set `max_iters` in such a way that the trainer will only perform " + f" {epochs} epochs. Please consider increasing this value accordingly") + if epochs <= 1: + logger.warning("Epochs set to 1. Please review your `max_iters` value") + epochs = 1 return epochs def save(self, path: Path): @@ -182,6 +188,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): "model_state_dict": self.model.state_dict(), "optimizer_state_dict": self.optimizer.state_dict(), "model_meta_data": self.model_meta_data, + "pytrainer": self }, path) def load(self, path: Path): @@ -195,7 +202,6 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): you can access this dict from any class that inherits IFreqaiModel by calling get_init_model method. """ - self.model.load_state_dict(checkpoint["model_state_dict"]) self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"]) self.model_meta_data = checkpoint["model_meta_data"] diff --git a/freqtrade/freqai/torch/PyTorchTrainerInterface.py b/freqtrade/freqai/torch/PyTorchTrainerInterface.py index 6686555f9..840c145f7 100644 --- a/freqtrade/freqai/torch/PyTorchTrainerInterface.py +++ b/freqtrade/freqai/torch/PyTorchTrainerInterface.py @@ -4,7 +4,7 @@ from typing import Dict, List import pandas as pd import torch -import torch.nn as nn +from torch import nn class PyTorchTrainerInterface(ABC): diff --git a/setup.sh b/setup.sh index a9ff36536..77c77000d 100755 --- a/setup.sh +++ b/setup.sh @@ -85,7 +85,7 @@ function updateenv() { if [[ $REPLY =~ ^[Yy]$ ]] then REQUIREMENTS_FREQAI="-r requirements-freqai.txt --use-pep517" - read -p "Do you also want dependencies for freqai-rl (~700mb additional space required) [y/N]? " + read -p "Do you also want dependencies for freqai-rl or PyTorch (~700mb additional space required) [y/N]? " if [[ $REPLY =~ ^[Yy]$ ]] then REQUIREMENTS_FREQAI="-r requirements-freqai-rl.txt" From c2c97d9f78801ede95191e799d480a3b943a14cd Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sat, 8 Apr 2023 13:20:29 +0200 Subject: [PATCH 154/208] make a fake pair_dict instead of MagicMocking it --- tests/freqai/test_freqai_interface.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 1122d9e9c..5c2d39e53 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -432,10 +432,12 @@ def test_plot_feature_importance(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) - freqai.dd.pair_dict = MagicMock() + freqai.dd.pair_dict = {"ADA/BTC": {"model_filename": "fake_name", + "trained_timestamp": 1, "data_path": "", "extras": {}}} data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") From 7751768b2e1131b837e95352228281e2f6f8290e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 14:30:27 +0200 Subject: [PATCH 155/208] Store initial_time value --- freqtrade/freqtradebot.py | 2 ++ freqtrade/persistence/key_value_store.py | 16 ++++++++++++++++ tests/persistence/test_key_value_store.py | 23 ++++++++++++++++++++++- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 48e3ec209..c4ef7794f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -26,6 +26,7 @@ from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, timeframe_to_minutes, time from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin from freqtrade.persistence import Order, PairLocks, Trade, init_db +from freqtrade.persistence.key_value_store import KeyValueStore, set_startup_time from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -182,6 +183,7 @@ class FreqtradeBot(LoggingMixin): performs startup tasks """ migrate_binance_futures_names(self.config) + set_startup_time() self.rpc.startup_messages(self.config, self.pairlists, self.protections) # Update older trades with precision and precision mode diff --git a/freqtrade/persistence/key_value_store.py b/freqtrade/persistence/key_value_store.py index 109f94fcc..0fdfc5aa6 100644 --- a/freqtrade/persistence/key_value_store.py +++ b/freqtrade/persistence/key_value_store.py @@ -104,3 +104,19 @@ class KeyValueStore(): if kv is not None: _KeyValueStoreModel.session.delete(kv) _KeyValueStoreModel.session.commit() + + +def set_startup_time(): + """ + sets bot_start_time to the first trade open date - or "now" on new databases. + sets startup_time to "now" + """ + st = KeyValueStore.get_value('bot_start_time') + if st is None: + from freqtrade.persistence import Trade + t = Trade.session.query(Trade).order_by(Trade.open_date.asc()).first() + if t is not None: + KeyValueStore.store_value('bot_start_time', t.open_date_utc) + else: + KeyValueStore.store_value('bot_start_time', datetime.now(timezone.utc)) + KeyValueStore.store_value('startup_time', datetime.now(timezone.utc)) diff --git a/tests/persistence/test_key_value_store.py b/tests/persistence/test_key_value_store.py index 8da5e4659..6113c57fa 100644 --- a/tests/persistence/test_key_value_store.py +++ b/tests/persistence/test_key_value_store.py @@ -2,7 +2,8 @@ from datetime import datetime, timedelta, timezone import pytest -from freqtrade.persistence.key_value_store import KeyValueStore +from freqtrade.persistence.key_value_store import KeyValueStore, set_startup_time +from tests.conftest import create_mock_trades_usdt @pytest.mark.usefixtures("init_persistence") @@ -37,3 +38,23 @@ def test_key_value_store(time_machine): with pytest.raises(ValueError, match=r"Unknown value type"): KeyValueStore.store_value("test_float", {'some': 'dict'}) + + +@pytest.mark.usefixtures("init_persistence") +def test_set_startup_time(fee, time_machine): + create_mock_trades_usdt(fee) + start = datetime.now(timezone.utc) + time_machine.move_to(start, tick=False) + set_startup_time() + + assert KeyValueStore.get_value("startup_time") == start + initial_time = KeyValueStore.get_value("bot_start_time") + assert initial_time <= start + + # Simulate bot restart + new_start = start + timedelta(days=5) + time_machine.move_to(new_start, tick=False) + set_startup_time() + + assert KeyValueStore.get_value("startup_time") == new_start + assert KeyValueStore.get_value("bot_start_time") == initial_time From 7ff30c6df8746618c0f0116bbf7fbc7fe4a39465 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 16:23:55 +0200 Subject: [PATCH 156/208] Add additional, typesafe getters --- freqtrade/persistence/key_value_store.py | 94 ++++++++++++++++++----- tests/persistence/test_key_value_store.py | 9 +++ 2 files changed, 82 insertions(+), 21 deletions(-) diff --git a/freqtrade/persistence/key_value_store.py b/freqtrade/persistence/key_value_store.py index 0fdfc5aa6..ddf94bfbf 100644 --- a/freqtrade/persistence/key_value_store.py +++ b/freqtrade/persistence/key_value_store.py @@ -44,27 +44,6 @@ class KeyValueStore(): Supports the types str, datetime, float and int. """ - @staticmethod - def get_value(key: str) -> Optional[ValueTypes]: - """ - Get the value for the given key. - :param key: Key to get the value for - """ - kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( - _KeyValueStoreModel.key == key).first() - if kv is None: - return None - if kv.value_type == ValueTypesEnum.STRING: - return kv.string_value - if kv.value_type == ValueTypesEnum.DATETIME and kv.datetime_value is not None: - return kv.datetime_value.replace(tzinfo=timezone.utc) - if kv.value_type == ValueTypesEnum.FLOAT: - return kv.float_value - if kv.value_type == ValueTypesEnum.INT: - return kv.int_value - # This should never happen unless someone messed with the database manually - raise ValueError(f'Unknown value type {kv.value_type}') # pragma: no cover - @staticmethod def store_value(key: str, value: ValueTypes) -> None: """ @@ -105,6 +84,79 @@ class KeyValueStore(): _KeyValueStoreModel.session.delete(kv) _KeyValueStoreModel.session.commit() + @staticmethod + def get_value(key: str) -> Optional[ValueTypes]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key).first() + if kv is None: + return None + if kv.value_type == ValueTypesEnum.STRING: + return kv.string_value + if kv.value_type == ValueTypesEnum.DATETIME and kv.datetime_value is not None: + return kv.datetime_value.replace(tzinfo=timezone.utc) + if kv.value_type == ValueTypesEnum.FLOAT: + return kv.float_value + if kv.value_type == ValueTypesEnum.INT: + return kv.int_value + # This should never happen unless someone messed with the database manually + raise ValueError(f'Unknown value type {kv.value_type}') # pragma: no cover + + @staticmethod + def get_string_value(key: str) -> Optional[str]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.STRING).first() + if kv is None: + return None + return kv.string_value + + @staticmethod + def get_datetime_value(key: str) -> Optional[datetime]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.DATETIME).first() + if kv is None or kv.datetime_value is None: + return None + return kv.datetime_value.replace(tzinfo=timezone.utc) + + @staticmethod + def get_float_value(key: str) -> Optional[float]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.FLOAT).first() + if kv is None: + return None + return kv.float_value + + @staticmethod + def get_int_value(key: str) -> Optional[int]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.INT).first() + if kv is None: + return None + return kv.int_value + def set_startup_time(): """ diff --git a/tests/persistence/test_key_value_store.py b/tests/persistence/test_key_value_store.py index 6113c57fa..1dab8764a 100644 --- a/tests/persistence/test_key_value_store.py +++ b/tests/persistence/test_key_value_store.py @@ -17,9 +17,18 @@ def test_key_value_store(time_machine): KeyValueStore.store_value("test_int", 15) assert KeyValueStore.get_value("test") == "testStringValue" + assert KeyValueStore.get_value("test") == "testStringValue" + assert KeyValueStore.get_string_value("test") == "testStringValue" assert KeyValueStore.get_value("test_dt") == datetime.now(timezone.utc) + assert KeyValueStore.get_datetime_value("test_dt") == datetime.now(timezone.utc) + assert KeyValueStore.get_string_value("test_dt") is None + assert KeyValueStore.get_float_value("test_dt") is None + assert KeyValueStore.get_int_value("test_dt") is None assert KeyValueStore.get_value("test_float") == 22.51 + assert KeyValueStore.get_float_value("test_float") == 22.51 assert KeyValueStore.get_value("test_int") == 15 + assert KeyValueStore.get_int_value("test_int") == 15 + assert KeyValueStore.get_datetime_value("test_int") is None time_machine.move_to(start + timedelta(days=20, hours=5), tick=False) assert KeyValueStore.get_value("test_dt") != datetime.now(timezone.utc) From fa3a81b02211a72d073e0fd8f6f2cdd261d6a4d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 16:28:50 +0200 Subject: [PATCH 157/208] convert Keys to enum --- freqtrade/persistence/key_value_store.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/freqtrade/persistence/key_value_store.py b/freqtrade/persistence/key_value_store.py index ddf94bfbf..2d26acbd3 100644 --- a/freqtrade/persistence/key_value_store.py +++ b/freqtrade/persistence/key_value_store.py @@ -18,6 +18,11 @@ class ValueTypesEnum(str, Enum): INT = 'int' +class KeyStoreKeys(str, Enum): + BOT_START_TIME = 'bot_start_time' + STARTUP_TIME = 'startup_time' + + class _KeyValueStoreModel(ModelBase): """ Pair Locks database model. @@ -27,9 +32,9 @@ class _KeyValueStoreModel(ModelBase): id: Mapped[int] = mapped_column(primary_key=True) - key: Mapped[str] = mapped_column(String(25), nullable=False, index=True) + key: Mapped[KeyStoreKeys] = mapped_column(String(25), nullable=False, index=True) - value_type: Mapped[ValueTypesEnum] = mapped_column(String(25), nullable=False) + value_type: Mapped[ValueTypesEnum] = mapped_column(String(20), nullable=False) string_value: Mapped[Optional[str]] datetime_value: Mapped[Optional[datetime]] @@ -45,7 +50,7 @@ class KeyValueStore(): """ @staticmethod - def store_value(key: str, value: ValueTypes) -> None: + def store_value(key: KeyStoreKeys, value: ValueTypes) -> None: """ Store the given value for the given key. :param key: Key to store the value for - can be used in get-value to retrieve the key @@ -73,7 +78,7 @@ class KeyValueStore(): _KeyValueStoreModel.session.commit() @staticmethod - def delete_value(key: str) -> None: + def delete_value(key: KeyStoreKeys) -> None: """ Delete the value for the given key. :param key: Key to delete the value for @@ -85,7 +90,7 @@ class KeyValueStore(): _KeyValueStoreModel.session.commit() @staticmethod - def get_value(key: str) -> Optional[ValueTypes]: + def get_value(key: KeyStoreKeys) -> Optional[ValueTypes]: """ Get the value for the given key. :param key: Key to get the value for @@ -106,7 +111,7 @@ class KeyValueStore(): raise ValueError(f'Unknown value type {kv.value_type}') # pragma: no cover @staticmethod - def get_string_value(key: str) -> Optional[str]: + def get_string_value(key: KeyStoreKeys) -> Optional[str]: """ Get the value for the given key. :param key: Key to get the value for @@ -119,7 +124,7 @@ class KeyValueStore(): return kv.string_value @staticmethod - def get_datetime_value(key: str) -> Optional[datetime]: + def get_datetime_value(key: KeyStoreKeys) -> Optional[datetime]: """ Get the value for the given key. :param key: Key to get the value for @@ -132,7 +137,7 @@ class KeyValueStore(): return kv.datetime_value.replace(tzinfo=timezone.utc) @staticmethod - def get_float_value(key: str) -> Optional[float]: + def get_float_value(key: KeyStoreKeys) -> Optional[float]: """ Get the value for the given key. :param key: Key to get the value for @@ -145,7 +150,7 @@ class KeyValueStore(): return kv.float_value @staticmethod - def get_int_value(key: str) -> Optional[int]: + def get_int_value(key: KeyStoreKeys) -> Optional[int]: """ Get the value for the given key. :param key: Key to get the value for From cf2cb94f8d28293373b856bcea71d6fc4229b329 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 16:38:44 +0200 Subject: [PATCH 158/208] Add bot start date to `/profit` output --- freqtrade/rpc/rpc.py | 6 +++++- freqtrade/rpc/telegram.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2b5eb107c..dc1c4c080 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -26,7 +26,8 @@ from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler from freqtrade.misc import decimals_per_coin, shorten_date -from freqtrade.persistence import Order, PairLocks, Trade +from freqtrade.persistence import KeyValueStore, Order, PairLocks, Trade +from freqtrade.persistence.key_value_store import KeyStoreKeys from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -543,6 +544,7 @@ class RPC: first_date = trades[0].open_date if trades else None last_date = trades[-1].open_date if trades else None num = float(len(durations) or 1) + bot_start = KeyValueStore.get_datetime_value(KeyStoreKeys.BOT_START_TIME) return { 'profit_closed_coin': profit_closed_coin_sum, 'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2), @@ -576,6 +578,8 @@ class RPC: 'max_drawdown': max_drawdown, 'max_drawdown_abs': max_drawdown_abs, 'trading_volume': trading_volume, + 'bot_start_timestamp': int(bot_start.timestamp() * 1000) if bot_start else 0, + 'bot_start_date': bot_start.strftime(DATETIME_PRINT_FORMAT) if bot_start else '', } def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index d79d8ea76..8637052de 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -819,7 +819,7 @@ class Telegram(RPCHandler): best_pair = stats['best_pair'] best_pair_profit_ratio = stats['best_pair_profit_ratio'] if stats['trade_count'] == 0: - markdown_msg = 'No trades yet.' + markdown_msg = f"No trades yet.\n*Bot started:* `{stats['bot_start_date']}`" else: # Message to display if stats['closed_trade_count'] > 0: @@ -838,6 +838,7 @@ class Telegram(RPCHandler): f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n" f"*Total Trade Count:* `{trade_count}`\n" + f"*Bot started:* `{stats['bot_start_date']}`\n" f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* " f"`{first_trade_date}`\n" f"*Latest Trade opened:* `{latest_trade_date}`\n" From be72670ca2ba979067b7486a0224d2f9c395e14a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 16:40:14 +0200 Subject: [PATCH 159/208] Add documentation about /profit change --- docs/telegram-usage.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index dc0ab0976..fe990790a 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -279,6 +279,7 @@ Return a summary of your profit/loss and performance. > ∙ `33.095 EUR` > > **Total Trade Count:** `138` +> **Bot started:** `2022-07-11 18:40:44` > **First Trade opened:** `3 days ago` > **Latest Trade opened:** `2 minutes ago` > **Avg. Duration:** `2:33:45` @@ -292,6 +293,7 @@ The relative profit of `15.2 Σ%` is be based on the starting capital - so in th Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits. Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy. Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`. +Bot started date will refer to the date the bot was first started. For older bots, this will default to the first trade's open date. ### /forceexit From a102cfdfc905a8bb7d76042a390198f7e6ca1d1a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 16:40:22 +0200 Subject: [PATCH 160/208] Add new /profit fields to API --- freqtrade/freqtradebot.py | 2 +- freqtrade/rpc/api_server/api_schemas.py | 2 ++ tests/rpc/test_rpc_apiserver.py | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c4ef7794f..73b25a7a1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -26,7 +26,7 @@ from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, timeframe_to_minutes, time from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin from freqtrade.persistence import Order, PairLocks, Trade, init_db -from freqtrade.persistence.key_value_store import KeyValueStore, set_startup_time +from freqtrade.persistence.key_value_store import set_startup_time from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 7497b27f1..53bf7558f 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -108,6 +108,8 @@ class Profit(BaseModel): max_drawdown: float max_drawdown_abs: float trading_volume: Optional[float] + bot_start_timestamp: int + bot_start_date: str class SellReason(BaseModel): diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 31075e514..21994d4cd 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -883,6 +883,8 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected) 'max_drawdown': ANY, 'max_drawdown_abs': ANY, 'trading_volume': expected['trading_volume'], + 'bot_start_timestamp': 0, + 'bot_start_date': '', } From f5a5c2d6b9b6c14ff878808f7e757272d0fc9d64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 16:44:33 +0200 Subject: [PATCH 161/208] Improve imports --- freqtrade/persistence/__init__.py | 2 +- freqtrade/rpc/rpc.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index c69a54b7f..4cf7aa455 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,6 +1,6 @@ # flake8: noqa: F401 -from freqtrade.persistence.key_value_store import KeyValueStore +from freqtrade.persistence.key_value_store import KeyStoreKeys, KeyValueStore from freqtrade.persistence.models import init_db from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.trade_model import LocalTrade, Order, Trade diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index dc1c4c080..05ea848f7 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -26,8 +26,7 @@ from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler from freqtrade.misc import decimals_per_coin, shorten_date -from freqtrade.persistence import KeyValueStore, Order, PairLocks, Trade -from freqtrade.persistence.key_value_store import KeyStoreKeys +from freqtrade.persistence import KeyStoreKeys, KeyValueStore, Order, PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter From bed51fa7902a99f7a3221a509b34a82ec120ad82 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 16:59:17 +0200 Subject: [PATCH 162/208] Properly build specific Torch image --- build_helpers/publish_docker_arm64.sh | 6 ++++++ build_helpers/publish_docker_multi.sh | 2 -- docs/freqai-configuration.md | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/build_helpers/publish_docker_arm64.sh b/build_helpers/publish_docker_arm64.sh index 229325efb..8f0de2cc9 100755 --- a/build_helpers/publish_docker_arm64.sh +++ b/build_helpers/publish_docker_arm64.sh @@ -12,6 +12,7 @@ TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG_PLOT=${TAG}_plot TAG_FREQAI=${TAG}_freqai TAG_FREQAI_RL=${TAG_FREQAI}rl +TAG_FREQAI_TORCH=${TAG_FREQAI}torch TAG_PI="${TAG}_pi" TAG_ARM=${TAG}_arm @@ -84,6 +85,10 @@ docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI} docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL} +# Create special Torch tag - which is identical to the RL tag. +docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_TORCH} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} +docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_TORCH} + # copy images to ghcr.io alias crane="docker run --rm -i -v $(pwd)/.crane:/home/nonroot/.docker/ gcr.io/go-containerregistry/crane" @@ -93,6 +98,7 @@ chmod a+rwx .crane echo "${GHCR_TOKEN}" | crane auth login ghcr.io -u "${GHCR_USERNAME}" --password-stdin crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL} +crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_TORCH} crane copy ${IMAGE_NAME}:${TAG_FREQAI} ${GHCR_IMAGE_NAME}:${TAG_FREQAI} crane copy ${IMAGE_NAME}:${TAG_PLOT} ${GHCR_IMAGE_NAME}:${TAG_PLOT} crane copy ${IMAGE_NAME}:${TAG} ${GHCR_IMAGE_NAME}:${TAG} diff --git a/build_helpers/publish_docker_multi.sh b/build_helpers/publish_docker_multi.sh index 3cbe9609b..72b20ac5d 100755 --- a/build_helpers/publish_docker_multi.sh +++ b/build_helpers/publish_docker_multi.sh @@ -9,7 +9,6 @@ TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG_PLOT=${TAG}_plot TAG_FREQAI=${TAG}_freqai TAG_FREQAI_RL=${TAG_FREQAI}rl -TAG_FREQAI_RL=${TAG_FREQAI}torch TAG_PI="${TAG}_pi" PI_PLATFORM="linux/arm/v7" @@ -66,7 +65,6 @@ docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQA docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI docker tag freqtrade:$TAG_FREQAI_RL ${CACHE_IMAGE}:$TAG_FREQAI_RL -docker tag freqtrade:$TAG_FREQAI_RL ${CACHE_IMAGE}:$TAG_FREQAI_TORCH # Run backtest docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3 diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 8f1aa5079..233edf2c5 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -254,6 +254,7 @@ freqtrade trade --config config_examples/config_freqai.example.json --strategy F ### Structure #### Model + You can construct your own Neural Network architecture in PyTorch by simply defining your `nn.Module` class inside your custom [`IFreqaiModel` file](#using-different-prediction-models) and then using that class in your `def train()` function. Here is an example of logistic regression model implementation using PyTorch (should be used with nn.BCELoss criterion) for classification tasks. ```python @@ -322,6 +323,7 @@ class MyCoolPyTorchClassifier(BasePyTorchClassifier): ``` #### Trainer + The `PyTorchModelTrainer` performs the idiomatic PyTorch train loop: Define our model, loss function, and optimizer, and then move them to the appropriate device (GPU or CPU). Inside the loop, we iterate through the batches in the dataloader, move the data to the device, compute the prediction and loss, backpropagate, and update the model parameters using the optimizer. @@ -330,6 +332,7 @@ In addition, the trainer is responsible for the following: - converting the data from `pandas.DataFrame` to `torch.Tensor`. #### Integration with Freqai module + Like all freqai models, PyTorch models inherit `IFreqaiModel`. `IFreqaiModel` declares three abstract methods: `train`, `fit`, and `predict`. we implement these methods in three levels of hierarchy. From top to bottom: @@ -340,6 +343,7 @@ From top to bottom: ![image](assets/freqai_pytorch-diagram.png) #### Full example + Building a PyTorch regressor using MLP (multilayer perceptron) model, MSELoss criterion, and AdamW optimizer. ```python From 5404905d2828d814def95353b258cef389448655 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 17:13:51 +0200 Subject: [PATCH 163/208] Fix typos in docs --- docs/freqai-parameter-table.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 6c95892d4..9ed3d6dce 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -88,19 +88,20 @@ Mandatory parameters are marked as **Required** and have to be set in one of the ### PyTorch parameters -#### general: +#### general | Parameter | Description | |------------|-------------| -| | **Model training parameters within the freqai.model_training_parameters sub dictionary** -| `learning_rate` | learning rate to be passed to the optimizer.
**Datatype:** float.
Default: `3e-4`. -| `model_kwargs` | paramters to be passed to the model class.
**Datatype:** dict.
Default: `{}`. -| `trainer_kwargs` | paramters to be passed to the trainer class.
**Datatype:** dict.
Default: `{}`. +| | **Model training parameters within the `freqai.model_training_parameters` sub dictionary** +| `learning_rate` | Learning rate to be passed to the optimizer.
**Datatype:** float.
Default: `3e-4`. +| `model_kwargs` | Parameters to be passed to the model class.
**Datatype:** dict.
Default: `{}`. +| `trainer_kwargs` | Parameters to be passed to the trainer class.
**Datatype:** dict.
Default: `{}`. -#### trainer_kwargs: +#### trainer_kwargs | Parameter | Description | |------------|-------------| +| | **Model training parameters within the `freqai.model_training_parameters.model_kwargs` sub dictionary** | `max_iters` | The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs.
**Datatype:** int.
Default: `100`. | `batch_size` | The size of the batches to use during training..
**Datatype:** int.
Default: `64`. | `max_n_eval_batches` | The maximum number batches to use for evaluation..
**Datatype:** int, optional.
Default: `None`. From dd8900a1c67c6d9c6ef4361472ed6dec27e148bd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Apr 2023 08:37:44 +0200 Subject: [PATCH 164/208] Improve ordering of backtest output --- docs/backtesting.md | 15 ++++++++------- freqtrade/optimize/optimize_reports.py | 10 +++++----- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 0227df3f6..166c2b28b 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -274,19 +274,20 @@ A backtesting result will look like that: | XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 | | ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 | | TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | -========================================================= EXIT REASON STATS ========================================================== -| Exit Reason | Exits | Wins | Draws | Losses | -|:-------------------|--------:|------:|-------:|--------:| -| trailing_stop_loss | 205 | 150 | 0 | 55 | -| stop_loss | 166 | 0 | 0 | 166 | -| exit_signal | 56 | 36 | 0 | 20 | -| force_exit | 2 | 0 | 0 | 2 | ====================================================== LEFT OPEN TRADES REPORT ====================================================== | Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% | |:---------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:| | ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 | | LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 | | TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 | +==================== EXIT REASON STATS ==================== +| Exit Reason | Exits | Wins | Draws | Losses | +|:-------------------|--------:|------:|-------:|--------:| +| trailing_stop_loss | 205 | 150 | 0 | 55 | +| stop_loss | 166 | 0 | 0 | 166 | +| exit_signal | 56 | 36 | 0 | 20 | +| force_exit | 2 | 0 | 0 | 2 | + ================== SUMMARY METRICS ================== | Metric | Value | |-----------------------------+---------------------| diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 83f698fbe..5cccd76a2 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -865,6 +865,11 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(table) + table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) + if isinstance(table, str) and len(table) > 0: + print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) + print(table) + if (results.get('results_per_enter_tag') is not None or results.get('results_per_buy_tag') is not None): # results_per_buy_tag is deprecated and should be removed 2 versions after short golive. @@ -884,11 +889,6 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' EXIT REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) - table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) - if isinstance(table, str) and len(table) > 0: - print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) - print(table) - for period in backtest_breakdown: days_breakdown_stats = generate_periodic_breakdown_stats( trade_list=results['trades'], period=period) From df51111c339796a7e9c2c7740b98c1fa3c683090 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Apr 2023 08:38:33 +0200 Subject: [PATCH 165/208] Always show strategy summary --- freqtrade/optimize/optimize_reports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 5cccd76a2..b4925770d 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -917,11 +917,11 @@ def show_backtest_results(config: Config, backtest_stats: Dict): strategy, results, stake_currency, config.get('backtest_breakdown', [])) - if len(backtest_stats['strategy']) > 1: + if len(backtest_stats['strategy']) > 0: # Print Strategy summary table table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency) - print(f"{results['backtest_start']} -> {results['backtest_end']} |" + print(f"Backtested {results['backtest_start']} -> {results['backtest_end']} |" f" Max open trades : {results['max_open_trades']}") print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '=')) print(table) From d532da90710df33752bca88959efa61fbfe456cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Apr 2023 16:04:31 +0200 Subject: [PATCH 166/208] Add Rich Progressbar Wrapper --- freqtrade/util/rich_progress.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 freqtrade/util/rich_progress.py diff --git a/freqtrade/util/rich_progress.py b/freqtrade/util/rich_progress.py new file mode 100644 index 000000000..7fe00b4d8 --- /dev/null +++ b/freqtrade/util/rich_progress.py @@ -0,0 +1,27 @@ +import logging +import sys +from contextlib import contextmanager + +from rich.progress import Progress + + +@contextmanager +def FtProgress(*args, **kwargs): + """ + Wrapper around rich.progress.Progress to fix issues with logging. + """ + try: + __logger = kwargs.pop('logger', None) + streamhandlers = [x for x in __logger.root.handlers if type(x) == logging.StreamHandler] + __prior_stderr = [] + + with Progress(*args, **kwargs) as progress: + for handler in streamhandlers: + __prior_stderr.append(handler.stream) + handler.setStream(sys.stderr) + + yield progress + + finally: + for idx, handler in enumerate(streamhandlers): + handler.setStream(__prior_stderr[idx]) From 40450ebeccb1d32f2b168d0d9da93999510bc263 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Apr 2023 16:05:10 +0200 Subject: [PATCH 167/208] Add dependency on Rich --- freqtrade/util/__init__.py | 1 + requirements.txt | 1 + setup.py | 1 + 3 files changed, 3 insertions(+) diff --git a/freqtrade/util/__init__.py b/freqtrade/util/__init__.py index 3c3c034c1..50527eb97 100644 --- a/freqtrade/util/__init__.py +++ b/freqtrade/util/__init__.py @@ -1,2 +1,3 @@ from freqtrade.util.ft_precise import FtPrecise # noqa: F401 from freqtrade.util.periodic_cache import PeriodicCache # noqa: F401 +from freqtrade.util.rich_progress import FtProgress # noqa: F401 diff --git a/requirements.txt b/requirements.txt index 34c7da0fa..f4f9b13fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,7 @@ jinja2==3.1.2 tables==3.8.0 blosc==1.11.1 joblib==1.2.0 +rich==13.3.3 pyarrow==11.0.0; platform_machine != 'armv7l' # find first, C search in arrays diff --git a/setup.py b/setup.py index 131e8a8a7..5c95e9316 100644 --- a/setup.py +++ b/setup.py @@ -82,6 +82,7 @@ setup( 'numpy', 'pandas', 'joblib>=1.2.0', + 'rich', 'pyarrow; platform_machine != "armv7l"', 'fastapi', 'pydantic>=1.8.0', From b6aac5079beb3f4f06320664665426406f7a1ba1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Apr 2023 16:21:30 +0200 Subject: [PATCH 168/208] REmove Rich-progress wrapper again --- freqtrade/util/__init__.py | 1 - freqtrade/util/rich_progress.py | 27 --------------------------- 2 files changed, 28 deletions(-) delete mode 100644 freqtrade/util/rich_progress.py diff --git a/freqtrade/util/__init__.py b/freqtrade/util/__init__.py index 50527eb97..3c3c034c1 100644 --- a/freqtrade/util/__init__.py +++ b/freqtrade/util/__init__.py @@ -1,3 +1,2 @@ from freqtrade.util.ft_precise import FtPrecise # noqa: F401 from freqtrade.util.periodic_cache import PeriodicCache # noqa: F401 -from freqtrade.util.rich_progress import FtProgress # noqa: F401 diff --git a/freqtrade/util/rich_progress.py b/freqtrade/util/rich_progress.py deleted file mode 100644 index 7fe00b4d8..000000000 --- a/freqtrade/util/rich_progress.py +++ /dev/null @@ -1,27 +0,0 @@ -import logging -import sys -from contextlib import contextmanager - -from rich.progress import Progress - - -@contextmanager -def FtProgress(*args, **kwargs): - """ - Wrapper around rich.progress.Progress to fix issues with logging. - """ - try: - __logger = kwargs.pop('logger', None) - streamhandlers = [x for x in __logger.root.handlers if type(x) == logging.StreamHandler] - __prior_stderr = [] - - with Progress(*args, **kwargs) as progress: - for handler in streamhandlers: - __prior_stderr.append(handler.stream) - handler.setStream(sys.stderr) - - yield progress - - finally: - for idx, handler in enumerate(streamhandlers): - handler.setStream(__prior_stderr[idx]) From 818d18d4e0922ce70f5c0dd2797c9bfc38c3f926 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Apr 2023 16:23:00 +0200 Subject: [PATCH 169/208] Add StdErrStreamHandler to logging --- freqtrade/loggers.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index 823fa174e..a9ba7d7d8 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -1,12 +1,36 @@ import logging import sys -from logging import Formatter +from logging import Formatter, Handler from logging.handlers import BufferingHandler, RotatingFileHandler, SysLogHandler from freqtrade.constants import Config from freqtrade.exceptions import OperationalException +class FTStdErrStreamHandler(Handler): + def flush(self): + """ + Override Flush behaviour - we keep half of the configured capacity + otherwise, we have moments with "empty" logs. + """ + self.acquire() + try: + sys.stderr.flush() + finally: + self.release() + + def emit(self, record): + try: + msg = self.format(record) + # Don't keep a reference to stderr - this can be problematic with progressbars. + sys.stderr.write(msg + '\n') + self.flush() + except RecursionError: + raise + except Exception: + self.handleError(record) + + class FTBufferingHandler(BufferingHandler): def flush(self): """ @@ -69,7 +93,7 @@ def setup_logging_pre() -> None: logging.basicConfig( level=logging.INFO, format=LOGFORMAT, - handlers=[logging.StreamHandler(sys.stderr), bufferHandler] + handlers=[FTStdErrStreamHandler(), bufferHandler] ) From ed57e7d43bc8bd08ecb5ada5fd5cc6a0f8465e2b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Apr 2023 16:48:18 +0200 Subject: [PATCH 170/208] Refactor logging to be a package, instead of a module --- freqtrade/{loggers.py => loggers/__init__.py} | 45 ++----------------- freqtrade/loggers/buffering_handler.py | 15 +++++++ freqtrade/loggers/std_err_stream_handler.py | 26 +++++++++++ 3 files changed, 45 insertions(+), 41 deletions(-) rename freqtrade/{loggers.py => loggers/__init__.py} (79%) create mode 100644 freqtrade/loggers/buffering_handler.py create mode 100644 freqtrade/loggers/std_err_stream_handler.py diff --git a/freqtrade/loggers.py b/freqtrade/loggers/__init__.py similarity index 79% rename from freqtrade/loggers.py rename to freqtrade/loggers/__init__.py index a9ba7d7d8..528d274f2 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers/__init__.py @@ -1,48 +1,11 @@ import logging -import sys -from logging import Formatter, Handler -from logging.handlers import BufferingHandler, RotatingFileHandler, SysLogHandler +from logging import Formatter +from logging.handlers import RotatingFileHandler, SysLogHandler from freqtrade.constants import Config from freqtrade.exceptions import OperationalException - - -class FTStdErrStreamHandler(Handler): - def flush(self): - """ - Override Flush behaviour - we keep half of the configured capacity - otherwise, we have moments with "empty" logs. - """ - self.acquire() - try: - sys.stderr.flush() - finally: - self.release() - - def emit(self, record): - try: - msg = self.format(record) - # Don't keep a reference to stderr - this can be problematic with progressbars. - sys.stderr.write(msg + '\n') - self.flush() - except RecursionError: - raise - except Exception: - self.handleError(record) - - -class FTBufferingHandler(BufferingHandler): - def flush(self): - """ - Override Flush behaviour - we keep half of the configured capacity - otherwise, we have moments with "empty" logs. - """ - self.acquire() - try: - # Keep half of the records in buffer. - self.buffer = self.buffer[-int(self.capacity / 2):] - finally: - self.release() +from freqtrade.loggers.buffering_handler import FTBufferingHandler +from freqtrade.loggers.std_err_stream_handler import FTStdErrStreamHandler logger = logging.getLogger(__name__) diff --git a/freqtrade/loggers/buffering_handler.py b/freqtrade/loggers/buffering_handler.py new file mode 100644 index 000000000..e4621fa79 --- /dev/null +++ b/freqtrade/loggers/buffering_handler.py @@ -0,0 +1,15 @@ +from logging.handlers import BufferingHandler + + +class FTBufferingHandler(BufferingHandler): + def flush(self): + """ + Override Flush behaviour - we keep half of the configured capacity + otherwise, we have moments with "empty" logs. + """ + self.acquire() + try: + # Keep half of the records in buffer. + self.buffer = self.buffer[-int(self.capacity / 2):] + finally: + self.release() diff --git a/freqtrade/loggers/std_err_stream_handler.py b/freqtrade/loggers/std_err_stream_handler.py new file mode 100644 index 000000000..487a7c100 --- /dev/null +++ b/freqtrade/loggers/std_err_stream_handler.py @@ -0,0 +1,26 @@ +import sys +from logging import Handler + + +class FTStdErrStreamHandler(Handler): + def flush(self): + """ + Override Flush behaviour - we keep half of the configured capacity + otherwise, we have moments with "empty" logs. + """ + self.acquire() + try: + sys.stderr.flush() + finally: + self.release() + + def emit(self, record): + try: + msg = self.format(record) + # Don't keep a reference to stderr - this can be problematic with progressbars. + sys.stderr.write(msg + '\n') + self.flush() + except RecursionError: + raise + except Exception: + self.handleError(record) From 4c1de4ad56c4d4ade00ee6cb646d24043563f793 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Apr 2023 18:07:28 +0200 Subject: [PATCH 171/208] Update tests --- tests/test_configuration.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index aab868bec..c445b989d 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -23,7 +23,8 @@ from freqtrade.configuration.load_config import (load_config_file, load_file, lo from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_VAR_PREFIX from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException -from freqtrade.loggers import FTBufferingHandler, _set_loggers, setup_logging, setup_logging_pre +from freqtrade.loggers import (FTBufferingHandler, FTStdErrStreamHandler, _set_loggers, + setup_logging, setup_logging_pre) from tests.conftest import (CURRENT_TEST_STRATEGY, log_has, log_has_re, patched_configuration_load_config_file) @@ -658,7 +659,7 @@ def test_set_loggers_syslog(): setup_logging(config) assert len(logger.handlers) == 3 assert [x for x in logger.handlers if type(x) == logging.handlers.SysLogHandler] - assert [x for x in logger.handlers if type(x) == logging.StreamHandler] + assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler] assert [x for x in logger.handlers if type(x) == FTBufferingHandler] # setting up logging again should NOT cause the loggers to be added a second time. setup_logging(config) @@ -681,7 +682,7 @@ def test_set_loggers_Filehandler(tmpdir): setup_logging(config) assert len(logger.handlers) == 3 assert [x for x in logger.handlers if type(x) == logging.handlers.RotatingFileHandler] - assert [x for x in logger.handlers if type(x) == logging.StreamHandler] + assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler] assert [x for x in logger.handlers if type(x) == FTBufferingHandler] # setting up logging again should NOT cause the loggers to be added a second time. setup_logging(config) @@ -706,7 +707,7 @@ def test_set_loggers_journald(mocker): setup_logging(config) assert len(logger.handlers) == 3 assert [x for x in logger.handlers if type(x).__name__ == "JournaldLogHandler"] - assert [x for x in logger.handlers if type(x) == logging.StreamHandler] + assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler] # reset handlers to not break pytest logger.handlers = orig_handlers From 299e78889143bf5b019a10874284bf40c81c9186 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Apr 2023 17:01:07 +0200 Subject: [PATCH 172/208] Dump progressbar2 dependency --- requirements-hyperopt.txt | 1 - setup.py | 1 - 2 files changed, 2 deletions(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 2c7c27d98..f7ee91663 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -6,4 +6,3 @@ scipy==1.10.1 scikit-learn==1.1.3 scikit-optimize==0.9.0 filelock==3.10.6 -progressbar2==4.2.0 diff --git a/setup.py b/setup.py index 5c95e9316..048dc066d 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,6 @@ hyperopt = [ 'scikit-learn', 'scikit-optimize>=0.7.0', 'filelock', - 'progressbar2', ] freqai = [ From bfd9e35e34f2a0a6b3c0ff8547390be8cb551c0a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Apr 2023 18:09:15 +0200 Subject: [PATCH 173/208] Replace hyperopt progressbar with rich progressbar --- freqtrade/optimize/hyperopt.py | 50 ++++++++++------------------------ 1 file changed, 15 insertions(+), 35 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 96c95c4a2..53d85dfd1 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -13,13 +13,13 @@ from math import ceil from pathlib import Path from typing import Any, Dict, List, Optional, Tuple -import progressbar import rapidjson -from colorama import Fore, Style from colorama import init as colorama_init from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects from joblib.externals import cloudpickle from pandas import DataFrame +from rich.progress import (BarColumn, MofNCompleteColumn, Progress, TaskProgressColumn, TextColumn, + TimeElapsedColumn, TimeRemainingColumn) from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config from freqtrade.data.converter import trim_dataframes @@ -44,8 +44,6 @@ with warnings.catch_warnings(): from skopt import Optimizer from skopt.space import Dimension -progressbar.streams.wrap_stderr() -progressbar.streams.wrap_stdout() logger = logging.getLogger(__name__) @@ -520,29 +518,6 @@ class Hyperopt: else: return self.opt.ask(n_points=n_points), [False for _ in range(n_points)] - def get_progressbar_widgets(self): - if self.print_colorized: - widgets = [ - ' [Epoch ', progressbar.Counter(), ' of ', str(self.total_epochs), - ' (', progressbar.Percentage(), ')] ', - progressbar.Bar(marker=progressbar.AnimatedMarker( - fill='\N{FULL BLOCK}', - fill_wrap=Fore.GREEN + '{}' + Fore.RESET, - marker_wrap=Style.BRIGHT + '{}' + Style.RESET_ALL, - )), - ' [', progressbar.ETA(), ', ', progressbar.Timer(), ']', - ] - else: - widgets = [ - ' [Epoch ', progressbar.Counter(), ' of ', str(self.total_epochs), - ' (', progressbar.Percentage(), ')] ', - progressbar.Bar(marker=progressbar.AnimatedMarker( - fill='\N{FULL BLOCK}', - )), - ' [', progressbar.ETA(), ', ', progressbar.Timer(), ']', - ] - return widgets - def evaluate_result(self, val: Dict[str, Any], current: int, is_random: bool): """ Evaluate results returned from generate_optimizer @@ -602,11 +577,18 @@ class Hyperopt: logger.info(f'Effective number of parallel workers used: {jobs}') # Define progressbar - widgets = self.get_progressbar_widgets() - with progressbar.ProgressBar( - max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False, - widgets=widgets + with Progress( + TextColumn("[progress.description]{task.description}"), + BarColumn(bar_width=None), + MofNCompleteColumn(), + TaskProgressColumn(), + TimeElapsedColumn(), + "<", + TimeRemainingColumn(), + expand=True, ) as pbar: + task = pbar.add_task("Epochs", total=self.total_epochs) + start = 0 if self.analyze_per_epoch: @@ -616,7 +598,7 @@ class Hyperopt: f_val0 = self.generate_optimizer(asked[0]) self.opt.tell(asked, [f_val0['loss']]) self.evaluate_result(f_val0, 1, is_random[0]) - pbar.update(1) + pbar.update(task, advance=1) start += 1 evals = ceil((self.total_epochs - start) / jobs) @@ -630,14 +612,12 @@ class Hyperopt: f_val = self.run_optimizer_parallel(parallel, asked) self.opt.tell(asked, [v['loss'] for v in f_val]) - # Calculate progressbar outputs for j, val in enumerate(f_val): # Use human-friendly indexes here (starting from 1) current = i * jobs + j + 1 + start self.evaluate_result(val, current, is_random[j]) - - pbar.update(current) + pbar.update(task, advance=1) except KeyboardInterrupt: print('User interrupted..') From cf770d496b6608d5a5f96256bc2ec2575eb9d729 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Apr 2023 18:25:50 +0200 Subject: [PATCH 174/208] Improve visual display of progressbar --- freqtrade/optimize/hyperopt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 53d85dfd1..ee5599e20 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -582,8 +582,9 @@ class Hyperopt: BarColumn(bar_width=None), MofNCompleteColumn(), TaskProgressColumn(), + "•", TimeElapsedColumn(), - "<", + "•", TimeRemainingColumn(), expand=True, ) as pbar: From 526943f29e6fc8ed46300c80cfdc43fce6df03c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 9 Apr 2023 19:44:38 +0200 Subject: [PATCH 175/208] Remove freqUI alpha warning --- docs/rest-api.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 5f604ef43..860a44499 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -9,9 +9,6 @@ This same command can also be used to update freqUI, should there be a new relea Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`). -!!! info "Alpha release" - FreqUI is still considered an alpha release - if you encounter bugs or inconsistencies please open a [FreqUI issue](https://github.com/freqtrade/frequi/issues/new/choose). - !!! Note "developers" Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI. From 8854ef8cba46571eb98a22a2cba6352ebfcf6ced Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 03:56:33 +0000 Subject: [PATCH 176/208] Bump ccxt from 3.0.50 to 3.0.58 Bumps [ccxt](https://github.com/ccxt/ccxt) from 3.0.50 to 3.0.58. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/3.0.50...3.0.58) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 34c7da0fa..7944a38f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.2 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==3.0.50 +ccxt==3.0.58 cryptography==40.0.1 aiohttp==3.8.4 SQLAlchemy==2.0.8 From a449f7c78c9702402b1ebd8730c0e26ef3d72b99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 03:56:38 +0000 Subject: [PATCH 177/208] Bump pytest from 7.2.2 to 7.3.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.2.2 to 7.3.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.2.2...7.3.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f36ef6def..e44e9357b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ coveralls==3.3.1 ruff==0.0.260 mypy==1.1.1 pre-commit==3.2.1 -pytest==7.2.2 +pytest==7.3.0 pytest-asyncio==0.21.0 pytest-cov==4.0.0 pytest-mock==3.10.0 From 14532e3a567eaa1e66be628e6460be0ffc27baf2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 03:56:42 +0000 Subject: [PATCH 178/208] Bump orjson from 3.8.9 to 3.8.10 Bumps [orjson](https://github.com/ijl/orjson) from 3.8.9 to 3.8.10. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.8.9...3.8.10) --- updated-dependencies: - dependency-name: orjson dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 34c7da0fa..0cd60a914 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.10 # Properly format api responses -orjson==3.8.9 +orjson==3.8.10 # Notify systemd sdnotify==0.3.2 From 7e1f3aa545cd2c06faeb8329640daba003485af8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 03:56:51 +0000 Subject: [PATCH 179/208] Bump filelock from 3.10.6 to 3.11.0 Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.10.6 to 3.11.0. - [Release notes](https://github.com/tox-dev/py-filelock/releases) - [Changelog](https://github.com/tox-dev/py-filelock/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/py-filelock/compare/3.10.6...3.11.0) --- updated-dependencies: - dependency-name: filelock dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 2c7c27d98..d617eeb0f 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -5,5 +5,5 @@ scipy==1.10.1 scikit-learn==1.1.3 scikit-optimize==0.9.0 -filelock==3.10.6 +filelock==3.11.0 progressbar2==4.2.0 From 26eb4f7fe69b0a6f829b840c9567e0c62d6f87d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 03:56:57 +0000 Subject: [PATCH 180/208] Bump pymdown-extensions from 9.10 to 9.11 Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 9.10 to 9.11. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/9.10...9.11) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index c70415c85..2c8d455df 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -2,5 +2,5 @@ markdown==3.3.7 mkdocs==1.4.2 mkdocs-material==9.1.5 mdx_truly_sane_lists==1.3 -pymdown-extensions==9.10 +pymdown-extensions==9.11 jinja2==3.1.2 From 03352f3b626aa4e9525c80088d6383487f3bfaf5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 03:57:04 +0000 Subject: [PATCH 181/208] Bump types-python-dateutil from 2.8.19.11 to 2.8.19.12 Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.19.11 to 2.8.19.12. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-python-dateutil dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f36ef6def..68f3ec934 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -29,4 +29,4 @@ types-cachetools==5.3.0.5 types-filelock==3.2.7 types-requests==2.28.11.17 types-tabulate==0.9.0.2 -types-python-dateutil==2.8.19.11 +types-python-dateutil==2.8.19.12 From 2ea01571970539f0e605cafe838dbe9c94df9265 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 03:57:51 +0000 Subject: [PATCH 182/208] Bump pypa/gh-action-pypi-publish from 1.8.4 to 1.8.5 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.4 to 1.8.5. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.4...v1.8.5) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e856607fc..52c772bd3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -425,7 +425,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@v1.8.4 + uses: pypa/gh-action-pypi-publish@v1.8.5 if: (github.event_name == 'release') with: user: __token__ @@ -433,7 +433,7 @@ jobs: repository_url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.4 + uses: pypa/gh-action-pypi-publish@v1.8.5 if: (github.event_name == 'release') with: user: __token__ From 5a18ab0784e8a480c0a1d994151969d693667314 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 05:51:33 +0000 Subject: [PATCH 183/208] Bump mypy from 1.1.1 to 1.2.0 Bumps [mypy](https://github.com/python/mypy) from 1.1.1 to 1.2.0. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v1.1.1...v1.2.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e44e9357b..3a1ff00a1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ coveralls==3.3.1 ruff==0.0.260 -mypy==1.1.1 +mypy==1.2.0 pre-commit==3.2.1 pytest==7.3.0 pytest-asyncio==0.21.0 From 3833dc0b78e18405e7b753aab58701eeee5f3f2d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Apr 2023 07:54:01 +0200 Subject: [PATCH 184/208] pre-commit - bump dateutil --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a5ac69ff4..4dad766ea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - types-filelock==3.2.7 - types-requests==2.28.11.17 - types-tabulate==0.9.0.2 - - types-python-dateutil==2.8.19.11 + - types-python-dateutil==2.8.19.12 - SQLAlchemy==2.0.8 # stages: [push] From f73d2a5371fd5bc33ac525622876fdf77d7e6de8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Apr 2023 14:47:59 +0200 Subject: [PATCH 185/208] Ensure bot_start is called when visualizing results --- freqtrade/rpc/rpc.py | 1 + tests/rpc/test_rpc_apiserver.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2b5eb107c..47651c540 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1193,6 +1193,7 @@ class RPC: from freqtrade.resolvers.strategy_resolver import StrategyResolver strategy = StrategyResolver.load_strategy(config) strategy.dp = DataProvider(config, exchange=exchange, pairlists=None) + strategy.ft_bot_start() df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 31075e514..698ccc5f3 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1403,10 +1403,10 @@ def test_api_pair_candles(botclient, ohlcv_history): ]) -def test_api_pair_history(botclient, ohlcv_history): +def test_api_pair_history(botclient, mocker): ftbot, client = botclient timeframe = '5m' - + lfm = mocker.patch('freqtrade.strategy.interface.IStrategy.load_freqAI_model') # No pair rc = client_get(client, f"{BASE_URI}/pair_history?timeframe={timeframe}" @@ -1440,6 +1440,7 @@ def test_api_pair_history(botclient, ohlcv_history): assert len(rc.json()['data']) == rc.json()['length'] assert 'columns' in rc.json() assert 'data' in rc.json() + assert lfm.call_count == 1 assert rc.json()['pair'] == 'UNITTEST/BTC' assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY assert rc.json()['data_start'] == '2018-01-11 00:00:00+00:00' From 351b5f6e65cd356398d14dd031aa32c4a98b20f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 14:19:56 +0000 Subject: [PATCH 186/208] Bump plotly from 5.14.0 to 5.14.1 Bumps [plotly](https://github.com/plotly/plotly.py) from 5.14.0 to 5.14.1. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v5.14.0...v5.14.1) --- updated-dependencies: - dependency-name: plotly dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index d87219c42..8b9ad5bc4 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,4 +1,4 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==5.14.0 +plotly==5.14.1 From 200c18f3e48d2fba5e070f3cc03825066c90f871 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 14:19:59 +0000 Subject: [PATCH 187/208] Bump schedule from 1.1.0 to 1.2.0 Bumps [schedule](https://github.com/dbader/schedule) from 1.1.0 to 1.2.0. - [Release notes](https://github.com/dbader/schedule/releases) - [Changelog](https://github.com/dbader/schedule/blob/master/HISTORY.rst) - [Commits](https://github.com/dbader/schedule/compare/1.1.0...1.2.0) --- updated-dependencies: - dependency-name: schedule dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 93daeb052..64e11541b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -50,7 +50,7 @@ prompt-toolkit==3.0.38 python-dateutil==2.8.2 #Futures -schedule==1.1.0 +schedule==1.2.0 #WS Messages websockets==11.0 From 66fe9abce0b9a890fe86850d65198c1aa60a0f41 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 14:20:03 +0000 Subject: [PATCH 188/208] Bump pre-commit from 3.2.1 to 3.2.2 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.2.1 to 3.2.2. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.2.1...v3.2.2) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c9bc9d244..c19f39b74 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ coveralls==3.3.1 ruff==0.0.260 mypy==1.2.0 -pre-commit==3.2.1 +pre-commit==3.2.2 pytest==7.3.0 pytest-asyncio==0.21.0 pytest-cov==4.0.0 From 230919777108b52f14801b0d03754582714cf04d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 14:20:14 +0000 Subject: [PATCH 189/208] Bump sqlalchemy from 2.0.8 to 2.0.9 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.8 to 2.0.9. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- updated-dependencies: - dependency-name: sqlalchemy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 93daeb052..8a8f77cf4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pandas-ta==0.3.14b ccxt==3.0.58 cryptography==40.0.1 aiohttp==3.8.4 -SQLAlchemy==2.0.8 +SQLAlchemy==2.0.9 python-telegram-bot==13.15 arrow==1.2.3 cachetools==4.2.2 From 0d408d3d438705212e36b142fc32fe4c737c28fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 14:20:19 +0000 Subject: [PATCH 190/208] Bump ccxt from 3.0.58 to 3.0.59 Bumps [ccxt](https://github.com/ccxt/ccxt) from 3.0.58 to 3.0.59. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/3.0.58...3.0.59) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 93daeb052..486ac0210 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.2 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==3.0.58 +ccxt==3.0.59 cryptography==40.0.1 aiohttp==3.8.4 SQLAlchemy==2.0.8 From e2cd23b1d2318bc5874e9fd8e78c3ca9add62f47 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Apr 2023 16:33:56 +0200 Subject: [PATCH 191/208] Remove deprecated pandas option --- freqtrade/data/btanalysis.py | 10 ++-------- freqtrade/data/converter.py | 2 +- freqtrade/data/history/featherdatahandler.py | 5 +---- freqtrade/data/history/jsondatahandler.py | 5 +---- freqtrade/data/history/parquetdatahandler.py | 5 +---- 5 files changed, 6 insertions(+), 21 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 3567f4112..c5905acde 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -246,14 +246,8 @@ def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame: """ Compatibility support for older backtest data. """ - df['open_date'] = pd.to_datetime(df['open_date'], - utc=True, - infer_datetime_format=True - ) - df['close_date'] = pd.to_datetime(df['close_date'], - utc=True, - infer_datetime_format=True - ) + df['open_date'] = pd.to_datetime(df['open_date'], utc=True) + df['close_date'] = pd.to_datetime(df['close_date'], utc=True) # Compatibility support for pre short Columns if 'is_short' not in df.columns: df['is_short'] = False diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 7ce98de42..2d3855d87 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -34,7 +34,7 @@ def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *, cols = DEFAULT_DATAFRAME_COLUMNS df = DataFrame(ohlcv, columns=cols) - df['date'] = to_datetime(df['date'], unit='ms', utc=True, infer_datetime_format=True) + df['date'] = to_datetime(df['date'], unit='ms', utc=True) # Some exchanges return int values for Volume and even for OHLC. # Convert them since TA-LIB indicators used in the strategy assume floats diff --git a/freqtrade/data/history/featherdatahandler.py b/freqtrade/data/history/featherdatahandler.py index bb387fc84..28a12fb29 100644 --- a/freqtrade/data/history/featherdatahandler.py +++ b/freqtrade/data/history/featherdatahandler.py @@ -63,10 +63,7 @@ class FeatherDataHandler(IDataHandler): pairdata.columns = self._columns pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float', 'volume': 'float'}) - pairdata['date'] = to_datetime(pairdata['date'], - unit='ms', - utc=True, - infer_datetime_format=True) + pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True) return pairdata def ohlcv_append( diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index f016c0ec1..ed7a33f8e 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -75,10 +75,7 @@ class JsonDataHandler(IDataHandler): return DataFrame(columns=self._columns) pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float', 'volume': 'float'}) - pairdata['date'] = to_datetime(pairdata['date'], - unit='ms', - utc=True, - infer_datetime_format=True) + pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True) return pairdata def ohlcv_append( diff --git a/freqtrade/data/history/parquetdatahandler.py b/freqtrade/data/history/parquetdatahandler.py index 57581861d..e6b2481d2 100644 --- a/freqtrade/data/history/parquetdatahandler.py +++ b/freqtrade/data/history/parquetdatahandler.py @@ -62,10 +62,7 @@ class ParquetDataHandler(IDataHandler): pairdata.columns = self._columns pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float', 'volume': 'float'}) - pairdata['date'] = to_datetime(pairdata['date'], - unit='ms', - utc=True, - infer_datetime_format=True) + pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True) return pairdata def ohlcv_append( From 467c63ff015e4008c3117c86b970e94607a712f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 15:25:04 +0000 Subject: [PATCH 192/208] Bump ruff from 0.0.260 to 0.0.261 Bumps [ruff](https://github.com/charliermarsh/ruff) from 0.0.260 to 0.0.261. - [Release notes](https://github.com/charliermarsh/ruff/releases) - [Changelog](https://github.com/charliermarsh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/charliermarsh/ruff/compare/v0.0.260...v0.0.261) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c19f39b74..28a50530f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.0.260 +ruff==0.0.261 mypy==1.2.0 pre-commit==3.2.2 pytest==7.3.0 From 70fa4a53cdc03313c7574b8db8c69fe921274d8c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Apr 2023 17:45:23 +0200 Subject: [PATCH 193/208] pre-commit - bump sqlalchemy --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4dad766ea..89370eacc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - types-requests==2.28.11.17 - types-tabulate==0.9.0.2 - types-python-dateutil==2.8.19.12 - - SQLAlchemy==2.0.8 + - SQLAlchemy==2.0.9 # stages: [push] - repo: https://github.com/pycqa/isort From fa293c54f857ae380bec39693a713bb403e3713d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 15:46:40 +0000 Subject: [PATCH 194/208] Bump websockets from 11.0 to 11.0.1 Bumps [websockets](https://github.com/aaugustin/websockets) from 11.0 to 11.0.1. - [Release notes](https://github.com/aaugustin/websockets/releases) - [Commits](https://github.com/aaugustin/websockets/compare/11.0...11.0.1) --- updated-dependencies: - dependency-name: websockets dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 64e11541b..a60ffedf5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -53,7 +53,7 @@ python-dateutil==2.8.2 schedule==1.2.0 #WS Messages -websockets==11.0 +websockets==11.0.1 janus==1.0.0 ast-comments==1.0.1 From 8564dc10b2e0b75ed03cb114fccd58fd62b90174 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 16:16:42 +0000 Subject: [PATCH 195/208] Bump nbconvert from 7.2.10 to 7.3.1 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 7.2.10 to 7.3.1. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Changelog](https://github.com/jupyter/nbconvert/blob/main/CHANGELOG.md) - [Commits](https://github.com/jupyter/nbconvert/compare/v7.2.10...v7.3.1) --- updated-dependencies: - dependency-name: nbconvert dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c19f39b74..48fc52459 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -22,7 +22,7 @@ time-machine==2.9.0 httpx==0.23.3 # Convert jupyter notebooks to markdown documents -nbconvert==7.2.10 +nbconvert==7.3.1 # mypy types types-cachetools==5.3.0.5 From c4c229868629b02a4d070df8ceb19a3509827dc3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 16:17:10 +0000 Subject: [PATCH 196/208] Bump mkdocs-material from 9.1.5 to 9.1.6 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.5 to 9.1.6. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.1.5...9.1.6) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 2c8d455df..550c3b54c 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.2 -mkdocs-material==9.1.5 +mkdocs-material==9.1.6 mdx_truly_sane_lists==1.3 pymdown-extensions==9.11 jinja2==3.1.2 From b892d373cd4858a65b711d454ede65266151408d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Apr 2023 17:44:21 +0200 Subject: [PATCH 197/208] Improve timerange parsing when accepting values from API --- freqtrade/configuration/timerange.py | 2 +- tests/test_timerange.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index adc5e65df..0c2f0d1b8 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -116,7 +116,7 @@ class TimeRange: :param text: value from --timerange :return: Start and End range period """ - if text is None: + if not text: return TimeRange(None, None, 0, 0) syntax = [(r'^-(\d{8})$', (None, 'date')), (r'^(\d{8})-$', ('date', None)), diff --git a/tests/test_timerange.py b/tests/test_timerange.py index 06ff1983a..993b24d95 100644 --- a/tests/test_timerange.py +++ b/tests/test_timerange.py @@ -10,6 +10,8 @@ from freqtrade.exceptions import OperationalException def test_parse_timerange_incorrect(): + timerange = TimeRange.parse_timerange('') + assert timerange == TimeRange(None, None, 0, 0) timerange = TimeRange.parse_timerange('20100522-') assert TimeRange('date', None, 1274486400, 0) == timerange assert timerange.timerange_str == '20100522-' From 40ffac9de09c19baaaa0388e388ae6a99ae56f18 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Apr 2023 19:45:13 +0200 Subject: [PATCH 198/208] Prevent random test failures by freezing time for certain tests --- tests/rpc/test_rpc_telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 54f612c59..cc83f96e0 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2241,8 +2241,9 @@ def test_send_msg_buy_notification_no_fiat( ('Short', 'short_signal_01', 2.0), ]) def test_send_msg_sell_notification_no_fiat( - default_conf, mocker, direction, enter_signal, leverage) -> None: + default_conf, mocker, direction, enter_signal, leverage, time_machine) -> None: del default_conf['fiat_display_currency'] + time_machine.move_to('2022-05-02 00:00:00 +00:00', tick=False) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ From 476ed938f583c8fd9a01ab02aa8056edbbd51058 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Apr 2023 07:26:38 +0200 Subject: [PATCH 199/208] Extract custom_tag limit from interface file --- freqtrade/constants.py | 1 + freqtrade/strategy/interface.py | 9 ++++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 1d12ed8c1..d83072064 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -64,6 +64,7 @@ USERPATH_FREQAIMODELS = 'freqaimodels' TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw'] FULL_DATAFRAME_THRESHOLD = 100 +CUSTOM_TAG_MAX_LENGTH = 64 ENV_VAR_PREFIX = 'FREQTRADE__' diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 6d4a3036f..3bc766d91 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -10,7 +10,7 @@ from typing import Dict, List, Optional, Tuple, Union import arrow from pandas import DataFrame -from freqtrade.constants import Config, IntOrInf, ListPairsWithTimeframes +from freqtrade.constants import CUSTOM_TAG_MAX_LENGTH, Config, IntOrInf, ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, RunMode, SignalDirection, SignalTagType, SignalType, TradingMode) @@ -27,7 +27,6 @@ from freqtrade.wallets import Wallets logger = logging.getLogger(__name__) -CUSTOM_EXIT_MAX_LENGTH = 64 class IStrategy(ABC, HyperStrategyMixin): @@ -1118,11 +1117,11 @@ class IStrategy(ABC, HyperStrategyMixin): exit_signal = ExitType.CUSTOM_EXIT if isinstance(reason_cust, str): custom_reason = reason_cust - if len(reason_cust) > CUSTOM_EXIT_MAX_LENGTH: + if len(reason_cust) > CUSTOM_TAG_MAX_LENGTH: logger.warning(f'Custom exit reason returned from ' f'custom_exit is too long and was trimmed' - f'to {CUSTOM_EXIT_MAX_LENGTH} characters.') - custom_reason = reason_cust[:CUSTOM_EXIT_MAX_LENGTH] + f'to {CUSTOM_TAG_MAX_LENGTH} characters.') + custom_reason = reason_cust[:CUSTOM_TAG_MAX_LENGTH] else: custom_reason = '' if ( From 9857675a5e019ff45fb70b09ad6ddd18bf832bb6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Apr 2023 19:38:24 +0200 Subject: [PATCH 200/208] Update torch import --- freqtrade/freqai/torch/PyTorchMLPModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/torch/PyTorchMLPModel.py b/freqtrade/freqai/torch/PyTorchMLPModel.py index e50ccd5ef..62d3216df 100644 --- a/freqtrade/freqai/torch/PyTorchMLPModel.py +++ b/freqtrade/freqai/torch/PyTorchMLPModel.py @@ -2,7 +2,7 @@ import logging from typing import List import torch -import torch.nn as nn +from torch import nn logger = logging.getLogger(__name__) From a6d2233b95a2cb62cb6d9228a333057cd49f12f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Apr 2023 21:05:14 +0200 Subject: [PATCH 201/208] Use constant for custom field lengths --- freqtrade/persistence/trade_model.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 628cb0220..a9d58f6ec 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -11,8 +11,8 @@ from sqlalchemy import (Enum, Float, ForeignKey, Integer, ScalarResult, Select, UniqueConstraint, desc, func, select) from sqlalchemy.orm import Mapped, lazyload, mapped_column, relationship -from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES, - BuySell, LongShort) +from freqtrade.constants import (CUSTOM_TAG_MAX_LENGTH, DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, + NON_OPEN_EXCHANGE_STATES, BuySell, LongShort) from freqtrade.enums import ExitType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision, @@ -1259,11 +1259,13 @@ class Trade(ModelBase, LocalTrade): Float(), nullable=True, default=0.0) # type: ignore # Lowest price reached min_rate: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore - exit_reason: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # type: ignore + exit_reason: Mapped[Optional[str]] = mapped_column( + String(CUSTOM_TAG_MAX_LENGTH), nullable=True) # type: ignore exit_order_status: Mapped[Optional[str]] = mapped_column( String(100), nullable=True) # type: ignore strategy: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # type: ignore - enter_tag: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # type: ignore + enter_tag: Mapped[Optional[str]] = mapped_column( + String(CUSTOM_TAG_MAX_LENGTH), nullable=True) # type: ignore timeframe: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # type: ignore trading_mode: Mapped[TradingMode] = mapped_column( From bba6f8e133456652ed15a8d3710d966d7172b064 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 12 Apr 2023 06:30:03 +0200 Subject: [PATCH 202/208] Use length constant for tests --- tests/strategy/test_interface.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 713ffa5cb..204fa996d 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -9,6 +9,7 @@ import pytest from pandas import DataFrame from freqtrade.configuration import TimeRange +from freqtrade.constants import CUSTOM_TAG_MAX_LENGTH from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data from freqtrade.enums import ExitCheckTuple, ExitType, HyperoptState, SignalDirection @@ -529,13 +530,13 @@ def test_custom_exit(default_conf, fee, caplog) -> None: assert res[0].exit_reason == 'hello world' caplog.clear() - strategy.custom_exit = MagicMock(return_value='h' * 100) + strategy.custom_exit = MagicMock(return_value='h' * CUSTOM_TAG_MAX_LENGTH * 2) res = strategy.should_exit(trade, 1, now, enter=False, exit_=False, low=None, high=None) assert res[0].exit_type == ExitType.CUSTOM_EXIT assert res[0].exit_flag is True - assert res[0].exit_reason == 'h' * 64 + assert res[0].exit_reason == 'h' * (CUSTOM_TAG_MAX_LENGTH) assert log_has_re('Custom exit reason returned from custom_exit is too long.*', caplog) From b2b19915e6e6180cd1602fc41a73d7f4428de91a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 12 Apr 2023 06:54:06 +0200 Subject: [PATCH 203/208] Limit enter_tag and exit_reason to their actual field lenght closes #8486 --- freqtrade/constants.py | 2 +- freqtrade/persistence/trade_model.py | 9 ++++++++- tests/persistence/test_persistence.py | 28 ++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index d83072064..2fcb957a3 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -64,7 +64,7 @@ USERPATH_FREQAIMODELS = 'freqaimodels' TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw'] FULL_DATAFRAME_THRESHOLD = 100 -CUSTOM_TAG_MAX_LENGTH = 64 +CUSTOM_TAG_MAX_LENGTH = 100 ENV_VAR_PREFIX = 'FREQTRADE__' diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index a9d58f6ec..0572b45a6 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -9,7 +9,7 @@ from typing import Any, ClassVar, Dict, List, Optional, Sequence, cast from sqlalchemy import (Enum, Float, ForeignKey, Integer, ScalarResult, Select, String, UniqueConstraint, desc, func, select) -from sqlalchemy.orm import Mapped, lazyload, mapped_column, relationship +from sqlalchemy.orm import Mapped, lazyload, mapped_column, relationship, validates from freqtrade.constants import (CUSTOM_TAG_MAX_LENGTH, DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort) @@ -1295,6 +1295,13 @@ class Trade(ModelBase, LocalTrade): self.realized_profit = 0 self.recalc_open_trade_value() + @validates('enter_tag', 'exit_reason') + def validate_string_len(self, key, value): + max_len = getattr(self.__class__, key).prop.columns[0].type.length + if value and len(value) > max_len: + return value[:max_len] + return value + def delete(self) -> None: for order in self.orders: diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 23ec6d4fb..948973ed5 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -6,7 +6,7 @@ import arrow import pytest from sqlalchemy import select -from freqtrade.constants import DATETIME_PRINT_FORMAT +from freqtrade.constants import CUSTOM_TAG_MAX_LENGTH, DATETIME_PRINT_FORMAT from freqtrade.enums import TradingMode from freqtrade.exceptions import DependencyException from freqtrade.persistence import LocalTrade, Order, Trade, init_db @@ -2037,6 +2037,7 @@ def test_Trade_object_idem(): 'get_mix_tag_performance', 'get_trading_volume', 'from_json', + 'validate_string_len', ) EXCLUDES2 = ('trades', 'trades_open', 'bt_trades_open_pp', 'bt_open_open_trade_count', 'total_profit') @@ -2055,6 +2056,31 @@ def test_Trade_object_idem(): assert item in trade +@pytest.mark.usefixtures("init_persistence") +def test_trade_truncates_string_fields(): + trade = Trade( + pair='ADA/USDT', + stake_amount=20.0, + amount=30.0, + open_rate=2.0, + open_date=datetime.utcnow() - timedelta(minutes=20), + fee_open=0.001, + fee_close=0.001, + exchange='binance', + leverage=1.0, + trading_mode='futures', + enter_tag='a' * CUSTOM_TAG_MAX_LENGTH * 2, + exit_reason='b' * CUSTOM_TAG_MAX_LENGTH * 2, + ) + Trade.session.add(trade) + Trade.commit() + + trade1 = Trade.session.scalars(select(Trade)).first() + + assert trade1.enter_tag == 'a' * CUSTOM_TAG_MAX_LENGTH + assert trade1.exit_reason == 'b' * CUSTOM_TAG_MAX_LENGTH + + def test_recalc_trade_from_orders(fee): o1_amount = 100 From 2131205db68644e11f3386f0797bf810c77eb1cd Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 12 Apr 2023 06:59:05 +0200 Subject: [PATCH 204/208] Bump tag length to 255 --- freqtrade/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 2fcb957a3..62b0f83be 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -64,7 +64,7 @@ USERPATH_FREQAIMODELS = 'freqaimodels' TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw'] FULL_DATAFRAME_THRESHOLD = 100 -CUSTOM_TAG_MAX_LENGTH = 100 +CUSTOM_TAG_MAX_LENGTH = 255 ENV_VAR_PREFIX = 'FREQTRADE__' From 44bf59668b9531324ffa35d11f1fb66810a3b30a Mon Sep 17 00:00:00 2001 From: Bloodhunter4rc Date: Wed, 12 Apr 2023 13:16:53 +0200 Subject: [PATCH 205/208] prevents continous fetching every x bot_loop seconds , adheres to refresh_period, in case the pairlist returned from the remote end is empty. --- freqtrade/plugins/pairlist/RemotePairList.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/freqtrade/plugins/pairlist/RemotePairList.py b/freqtrade/plugins/pairlist/RemotePairList.py index 764c16f1a..0bf81c89d 100644 --- a/freqtrade/plugins/pairlist/RemotePairList.py +++ b/freqtrade/plugins/pairlist/RemotePairList.py @@ -142,7 +142,10 @@ class RemotePairList(IPairList): """ if self._init_done: - pairlist = self._pair_cache.get('pairlist') + if self._pair_cache.get('pairlist') == ["Empty"]: + return [] + else: + pairlist = self._pair_cache.get('pairlist') else: pairlist = [] @@ -181,7 +184,10 @@ class RemotePairList(IPairList): pairlist = self._whitelist_for_active_markets(pairlist) pairlist = pairlist[:self._number_pairs] - self._pair_cache['pairlist'] = pairlist.copy() + if pairlist: + self._pair_cache['pairlist'] = pairlist.copy() + else: + self._pair_cache['pairlist'] = ["Empty"] if time_elapsed != 0.0: self.log_once(f'Pairlist Fetched in {time_elapsed} seconds.', logger.info) From 0afd5a7385fe05b9bca709a824e7a00659a3f62c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 12 Apr 2023 18:13:16 +0200 Subject: [PATCH 206/208] Improve stoploss documentation closes #8492 --- docs/stoploss.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 7af717955..d85902be0 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -23,10 +23,22 @@ These modes can be configured with these values: 'stoploss_on_exchange_limit_ratio': 0.99 ``` -!!! Note - Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), Gate (stop-limit), and Kucoin (stop-limit and stop-market) as of now. - Do not set too low/tight stoploss value if using stop loss on exchange! - If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work. +Stoploss on exchange is only supported for the following exchanges, and not all exchanges support both stop-limit and stop-market. +The Order-type will be ignored if only one mode is available. + +| Exchange | stop-loss type | +|----------|-------------| +| Binance | limit | +| Binance Futures | market, limit | +| Huobi | limit | +| kraken | market, limit | +| Gate | limit | +| Okx | limit | +| Kucoin | stop-limit, stop-market| + +!!! Note "Tight stoploss" + Do not set too low/tight stoploss value when using stop loss on exchange! + If set to low/tight you will have greater risk of missing fill on the order and stoploss will not work. ### stoploss_on_exchange and stoploss_on_exchange_limit_ratio From 84d2d5e2a636ea84524342f0ee4eb25a487cea3c Mon Sep 17 00:00:00 2001 From: Bloodhunter4rc Date: Wed, 12 Apr 2023 19:32:28 +0200 Subject: [PATCH 207/208] Change ["Dummy"] to [None]. --- freqtrade/plugins/pairlist/RemotePairList.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/plugins/pairlist/RemotePairList.py b/freqtrade/plugins/pairlist/RemotePairList.py index 0bf81c89d..24822bcd7 100644 --- a/freqtrade/plugins/pairlist/RemotePairList.py +++ b/freqtrade/plugins/pairlist/RemotePairList.py @@ -142,10 +142,10 @@ class RemotePairList(IPairList): """ if self._init_done: - if self._pair_cache.get('pairlist') == ["Empty"]: - return [] - else: + if self._pair_cache.get('pairlist') != [None]: pairlist = self._pair_cache.get('pairlist') + else: + return [] else: pairlist = [] @@ -187,7 +187,7 @@ class RemotePairList(IPairList): if pairlist: self._pair_cache['pairlist'] = pairlist.copy() else: - self._pair_cache['pairlist'] = ["Empty"] + self._pair_cache['pairlist'] = [None] if time_elapsed != 0.0: self.log_once(f'Pairlist Fetched in {time_elapsed} seconds.', logger.info) From 3b377149e4eebf6b3546c82ea29455c2b13ce2d0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Apr 2023 18:19:52 +0200 Subject: [PATCH 208/208] Add clarifying comment, simplify code --- freqtrade/plugins/pairlist/RemotePairList.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/plugins/pairlist/RemotePairList.py b/freqtrade/plugins/pairlist/RemotePairList.py index 24822bcd7..d077330e0 100644 --- a/freqtrade/plugins/pairlist/RemotePairList.py +++ b/freqtrade/plugins/pairlist/RemotePairList.py @@ -142,9 +142,9 @@ class RemotePairList(IPairList): """ if self._init_done: - if self._pair_cache.get('pairlist') != [None]: - pairlist = self._pair_cache.get('pairlist') - else: + pairlist = self._pair_cache.get('pairlist') + if pairlist == [None]: + # Valid but empty pairlist. return [] else: pairlist = [] @@ -187,6 +187,7 @@ class RemotePairList(IPairList): if pairlist: self._pair_cache['pairlist'] = pairlist.copy() else: + # If pairlist is empty, set a dummy value to avoid fetching again self._pair_cache['pairlist'] = [None] if time_elapsed != 0.0: