From 9cb45a38108fcd33352bf8c645adc2a46c4b2218 Mon Sep 17 00:00:00 2001 From: yinon Date: Thu, 13 Jul 2023 15:37:50 +0000 Subject: [PATCH 001/116] pytorch - bugfix - explicitly assign tensor to var as .to() is not inplace operation --- freqtrade/freqai/torch/PyTorchModelTrainer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index 603e7ac12..e74b572fd 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -83,8 +83,8 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): for i, batch_data in enumerate(data_loaders_dictionary["train"]): xb, yb = batch_data - xb.to(self.device) - yb.to(self.device) + xb = xb.to(self.device) + yb = yb.to(self.device) yb_pred = self.model(xb) loss = self.criterion(yb_pred, yb) From 0c9aa86885ca8019f2a1c73629f66d01b8817818 Mon Sep 17 00:00:00 2001 From: yinon Date: Thu, 13 Jul 2023 15:38:58 +0000 Subject: [PATCH 002/116] pytorch - data convertor - create tensor directly on device, simplify code --- .../freqai/torch/PyTorchDataConvertor.py | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchDataConvertor.py b/freqtrade/freqai/torch/PyTorchDataConvertor.py index e6b815373..0af14dd14 100644 --- a/freqtrade/freqai/torch/PyTorchDataConvertor.py +++ b/freqtrade/freqai/torch/PyTorchDataConvertor.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -from typing import Optional import pandas as pd import torch @@ -12,14 +11,14 @@ class PyTorchDataConvertor(ABC): """ @abstractmethod - def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor: + def convert_x(self, df: pd.DataFrame, device: str) -> torch.Tensor: """ :param df: "*_features" dataframe. :param device: The device to use for training (e.g. 'cpu', 'cuda'). """ @abstractmethod - def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor: + def convert_y(self, df: pd.DataFrame, device: str) -> torch.Tensor: """ :param df: "*_labels" dataframe. :param device: The device to use for training (e.g. 'cpu', 'cuda'). @@ -33,8 +32,8 @@ class DefaultPyTorchDataConvertor(PyTorchDataConvertor): def __init__( self, - target_tensor_type: Optional[torch.dtype] = None, - squeeze_target_tensor: bool = False + target_tensor_type: torch.dtype = torch.float32, + squeeze_target_tensor: bool = False, ): """ :param target_tensor_type: type of target tensor, for classification use @@ -45,23 +44,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) -> torch.Tensor: - x = torch.from_numpy(df.values).float() - if device: - x = x.to(device) - + def convert_x(self, df: pd.DataFrame, device: str) -> torch.Tensor: + numpy_arrays = df.values + x = torch.tensor(numpy_arrays, device=device, dtype=torch.float32) return x - def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor: - y = torch.from_numpy(df.values) - - if self._target_tensor_type: - y = y.to(self._target_tensor_type) - + def convert_y(self, df: pd.DataFrame, device: str) -> torch.Tensor: + numpy_arrays = df.values + y = torch.tensor(numpy_arrays, device=device, dtype=self._target_tensor_type) if self._squeeze_target_tensor: y = y.squeeze() - - if device: - y = y.to(device) - return y From 49a7de4ebdf7ea3621f072957ca936b26f82fc03 Mon Sep 17 00:00:00 2001 From: yinon Date: Thu, 13 Jul 2023 15:39:47 +0000 Subject: [PATCH 003/116] pytorch - trainer - add device arg to load method --- freqtrade/freqai/torch/PyTorchModelTrainer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index e74b572fd..b49e16196 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -182,8 +182,8 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): "pytrainer": self }, path) - def load(self, path: Path): - checkpoint = torch.load(path) + def load(self, path: Path, device: str = None): + checkpoint = torch.load(path, map_location=device) return self.load_from_checkpoint(checkpoint) def load_from_checkpoint(self, checkpoint: Dict): From 588ffeedc146c790a7eb81defe1bf0221d3ee934 Mon Sep 17 00:00:00 2001 From: yinon Date: Thu, 13 Jul 2023 15:40:40 +0000 Subject: [PATCH 004/116] pytorch - trainer - reomve max_n_eval_batches arg from estimate loss method --- freqtrade/freqai/torch/PyTorchModelTrainer.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index b49e16196..fe9919810 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -1,5 +1,4 @@ import logging -import math from pathlib import Path from typing import Any, Dict, List, Optional @@ -53,7 +52,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): self.device = device 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.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None) # TODO change this to n_batches self.data_convertor = data_convertor self.window_size: int = window_size self.tb_logger = tb_logger @@ -95,25 +94,16 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): # evaluation if "test" in splits: - self.estimate_loss( - data_loaders_dictionary, - self.max_n_eval_batches, - "test" - ) + self.estimate_loss(data_loaders_dictionary, "test") @torch.no_grad() def estimate_loss( self, data_loader_dictionary: Dict[str, DataLoader], - max_n_eval_batches: Optional[int], split: str, ) -> None: self.model.eval() - n_batches = 0 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_data xb.to(self.device) yb.to(self.device) @@ -158,8 +148,8 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): across different n_obs - the number of data points. """ - n_batches = math.ceil(n_obs // batch_size) - epochs = math.ceil(n_iters // n_batches) + n_batches = n_obs // batch_size + epochs = 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") From 7d28dad209784b48799ce9099bdd442b243e4632 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 13 Jul 2023 19:41:39 +0300 Subject: [PATCH 005/116] pytorch - add n_epochs param to trainer --- .../prediction_models/PyTorchMLPClassifier.py | 2 +- .../prediction_models/PyTorchMLPRegressor.py | 2 +- .../PyTorchTransformerRegressor.py | 2 +- freqtrade/freqai/torch/PyTorchModelTrainer.py | 19 ++++++++----------- tests/freqai/conftest.py | 2 +- 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 71279dba9..ca333d9cf 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -28,7 +28,7 @@ class PyTorchMLPClassifier(BasePyTorchClassifier): "trainer_kwargs": { "max_iters": 5000, "batch_size": 64, - "max_n_eval_batches": null, + "n_epochs": null, }, "model_kwargs": { "hidden_dim": 512, diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 9f4534487..42fddf8ff 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -29,7 +29,7 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): "trainer_kwargs": { "max_iters": 5000, "batch_size": 64, - "max_n_eval_batches": null, + "n_epochs": null, }, "model_kwargs": { "hidden_dim": 512, diff --git a/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py b/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py index a76bab05c..32663c86b 100644 --- a/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py @@ -32,7 +32,7 @@ class PyTorchTransformerRegressor(BasePyTorchRegressor): "trainer_kwargs": { "max_iters": 5000, "batch_size": 64, - "max_n_eval_batches": null + "n_epochs": null }, "model_kwargs": { "hidden_dim": 512, diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index fe9919810..a34d673b4 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -40,10 +40,10 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): :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. + iteration here refers to the number of times optimizer.step() is called, + used to calculate n_epochs. ignored if n_epochs is set. + :param n_epochs: The maximum number batches to use for evaluation. :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 @@ -51,8 +51,8 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): self.model_meta_data = model_meta_data self.device = device self.max_iters: int = kwargs.get("max_iters", 100) + self.n_epochs: Optional[int] = kwargs.get("n_epochs", None) self.batch_size: int = kwargs.get("batch_size", 64) - self.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None) # TODO change this to n_batches self.data_convertor = data_convertor self.window_size: int = window_size self.tb_logger = tb_logger @@ -71,16 +71,13 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): backpropagation. - Updates the model's parameters using an optimizer. """ - 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 - ) self.model.train() + + data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary, splits) + n_obs = len(data_dictionary["train_features"]) + epochs = self.n_epochs or self.calc_n_epochs(n_obs=n_obs, batch_size=self.batch_size, n_iters=self.max_iters) for epoch in range(1, epochs + 1): for i, batch_data in enumerate(data_loaders_dictionary["train"]): - xb, yb = batch_data xb = xb.to(self.device) yb = yb.to(self.device) diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index 4c4891ceb..96716e83f 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -99,7 +99,7 @@ def mock_pytorch_mlp_model_training_parameters() -> Dict[str, Any]: "trainer_kwargs": { "max_iters": 1, "batch_size": 64, - "max_n_eval_batches": 1, + "n_epochs": None, }, "model_kwargs": { "hidden_dim": 32, From 5734358d91399e5e4caac8c9722bf8a23165d863 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 13 Jul 2023 20:59:33 +0300 Subject: [PATCH 006/116] pytorch - trainer - add assertion that either n_epochs or max_iters is been set. --- freqtrade/freqai/torch/PyTorchModelTrainer.py | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index a34d673b4..efdf3ed5a 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -39,9 +39,9 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): 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 optimizer.step() is called, - used to calculate n_epochs. ignored if n_epochs is set. + :param max_iters: used to calculate n_epochs. The number of training iterations to run. + iteration here refers to the number of times optimizer.step() is called. + ignored if n_epochs is set. :param n_epochs: The maximum number batches to use for evaluation. :param batch_size: The size of the batches to use during training. """ @@ -52,6 +52,9 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): self.device = device self.max_iters: int = kwargs.get("max_iters", 100) self.n_epochs: Optional[int] = kwargs.get("n_epochs", None) + if not self.max_iters and not self.n_epochs: + raise Exception("Either `max_iters` or `n_epochs` should be set.") + self.batch_size: int = kwargs.get("batch_size", 64) self.data_convertor = data_convertor self.window_size: int = window_size @@ -75,8 +78,8 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary, splits) n_obs = len(data_dictionary["train_features"]) - epochs = self.n_epochs or self.calc_n_epochs(n_obs=n_obs, batch_size=self.batch_size, n_iters=self.max_iters) - for epoch in range(1, epochs + 1): + n_epochs = self.n_epochs or self.calc_n_epochs(n_obs=n_obs, batch_size=self.batch_size, n_iters=self.max_iters) + for epoch in range(1, n_epochs + 1): for i, batch_data in enumerate(data_loaders_dictionary["train"]): xb, yb = batch_data xb = xb.to(self.device) @@ -146,14 +149,14 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): """ n_batches = n_obs // batch_size - epochs = 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 + n_epochs = min(n_iters // n_batches, 1) + if n_epochs <= 10: + logger.warning( + f"Setting low n_epochs. {n_epochs} = n_epochs = n_iters // n_batches = {n_iters} // {n_batches}. " + f"Please consider increasing `max_iters` hyper-parameter." + ) + + return n_epochs def save(self, path: Path): """ From 9fb0ce664c76c02bdc15d351604385ad25bc43c9 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 13 Jul 2023 21:32:46 +0300 Subject: [PATCH 007/116] pytorch - ruff fixes --- freqtrade/freqai/torch/PyTorchModelTrainer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index efdf3ed5a..e6691f3db 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -78,7 +78,11 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary, splits) n_obs = len(data_dictionary["train_features"]) - n_epochs = self.n_epochs or self.calc_n_epochs(n_obs=n_obs, batch_size=self.batch_size, n_iters=self.max_iters) + n_epochs = self.n_epochs or self.calc_n_epochs( + n_obs=n_obs, + batch_size=self.batch_size, + n_iters=self.max_iters, + ) for epoch in range(1, n_epochs + 1): for i, batch_data in enumerate(data_loaders_dictionary["train"]): xb, yb = batch_data @@ -152,7 +156,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): n_epochs = min(n_iters // n_batches, 1) if n_epochs <= 10: logger.warning( - f"Setting low n_epochs. {n_epochs} = n_epochs = n_iters // n_batches = {n_iters} // {n_batches}. " + f"Setting low n_epochs: {n_epochs}. " f"Please consider increasing `max_iters` hyper-parameter." ) From ffcba45b1bda92a2d71b7a4d40254e2d0c352aa6 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 13 Jul 2023 21:36:14 +0300 Subject: [PATCH 008/116] pytorch - mypy fixes --- 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 e6691f3db..e6638d4fd 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -176,7 +176,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): "pytrainer": self }, path) - def load(self, path: Path, device: str = None): + def load(self, path: Path, device: Optional[str] = None): checkpoint = torch.load(path, map_location=device) return self.load_from_checkpoint(checkpoint) From 77f1584713a52751c9b621c3faf37c1cd8ba1f63 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sat, 15 Jul 2023 14:37:44 +0300 Subject: [PATCH 009/116] pytorch - trainer - bugfix step tensorboard step usage --- freqtrade/freqai/torch/PyTorchModelTrainer.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index e6638d4fd..1692b4acf 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -59,6 +59,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): self.data_convertor = data_convertor self.window_size: int = window_size self.tb_logger = tb_logger + self.test_batch_counter = 0 def fit(self, data_dictionary: Dict[str, pd.DataFrame], splits: List[str]): """ @@ -83,8 +84,10 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): batch_size=self.batch_size, n_iters=self.max_iters, ) - for epoch in range(1, n_epochs + 1): - for i, batch_data in enumerate(data_loaders_dictionary["train"]): + + batch_counter = 0 + for epoch in range(n_epochs): + for _, batch_data in enumerate(data_loaders_dictionary["train"]): xb, yb = batch_data xb = xb.to(self.device) yb = yb.to(self.device) @@ -94,7 +97,8 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): self.optimizer.zero_grad(set_to_none=True) loss.backward() self.optimizer.step() - self.tb_logger.log_scalar("train_loss", loss.item(), i) + self.tb_logger.log_scalar("train_loss", loss.item(), batch_counter) + batch_counter += 1 # evaluation if "test" in splits: @@ -107,14 +111,15 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): split: str, ) -> None: self.model.eval() - for i, batch_data in enumerate(data_loader_dictionary[split]): + for _, batch_data in enumerate(data_loader_dictionary[split]): xb, yb = batch_data xb.to(self.device) yb.to(self.device) yb_pred = self.model(xb) loss = self.criterion(yb_pred, yb) - self.tb_logger.log_scalar(f"{split}_loss", loss.item(), i) + self.tb_logger.log_scalar(f"{split}_loss", loss.item(), self.test_batch_counter) + self.test_batch_counter += 1 self.model.train() From d61f512e200c214d285aaff5775c54c46987d343 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sat, 15 Jul 2023 14:43:05 +0300 Subject: [PATCH 010/116] pytorch - trainer - clean code --- freqtrade/freqai/torch/PyTorchModelTrainer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index 1692b4acf..7a8857994 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -86,7 +86,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): ) batch_counter = 0 - for epoch in range(n_epochs): + for _ in range(n_epochs): for _, batch_data in enumerate(data_loaders_dictionary["train"]): xb, yb = batch_data xb = xb.to(self.device) @@ -171,7 +171,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): """ - 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. + user needs to store. e.g. class_names for classification models. """ torch.save({ From a5f5293bc86a0a9560c8ac2d2ae5f19c58feb0a3 Mon Sep 17 00:00:00 2001 From: hippocritical Date: Sun, 23 Jul 2023 11:23:02 +0200 Subject: [PATCH 011/116] added logger-output when something is skipped or aborted --- freqtrade/optimize/lookahead_analysis.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/freqtrade/optimize/lookahead_analysis.py b/freqtrade/optimize/lookahead_analysis.py index dcc1088b3..f8f9c7d55 100755 --- a/freqtrade/optimize/lookahead_analysis.py +++ b/freqtrade/optimize/lookahead_analysis.py @@ -251,9 +251,26 @@ class LookaheadAnalysis: # starting from the same datetime to avoid miss-reports of bias for idx, result_row in self.full_varHolder.result['results'].iterrows(): if self.current_analysis.total_signals == self.targeted_trade_amount: + logger.info(f"Found targeted trade amount = {self.targeted_trade_amount} signals.") break + if found_signals < self.minimum_trade_amount: + logger.info(f"only found {found_signals} " + f"which is smaller than " + f"minimum trade amount = {self.minimum_trade_amount}. " + f"Exiting this lookahead-analysis") + return None + if "force_exit" in result_row['exit_reason']: + logger.info("found force-exit, skipping this one to avoid a false-positive.") + continue + self.analyze_row(idx, result_row) + if len(self.entry_varHolders) < self.minimum_trade_amount: + logger.info(f"only found {found_signals} after skipping forced exits " + f"which is smaller than " + f"minimum trade amount = {self.minimum_trade_amount}. " + f"Exiting this lookahead-analysis") + # Restore verbosity, so it's not too quiet for the next strategy restore_verbosity_for_bias_tester() # check and report signals From a33be8a349b64067d9509dd783008450fd864027 Mon Sep 17 00:00:00 2001 From: hippocritical Date: Sun, 23 Jul 2023 13:48:54 +0200 Subject: [PATCH 012/116] added dummy-varholders in case a not-last-trade is force-exit and else the indexes would shift ruining the analysis and making debugging easier (since the same ID will always be the same ID again) --- freqtrade/optimize/lookahead_analysis.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/freqtrade/optimize/lookahead_analysis.py b/freqtrade/optimize/lookahead_analysis.py index f8f9c7d55..e87c3c7de 100755 --- a/freqtrade/optimize/lookahead_analysis.py +++ b/freqtrade/optimize/lookahead_analysis.py @@ -214,6 +214,7 @@ class LookaheadAnalysis: self.entry_varHolders[idx].result, "open_date", self.entry_varHolders[idx].compared_dt): + # logger.info(f"found lookahead-bias in trade {self.entry_varHolders[idx][]} {idx}") self.current_analysis.false_entry_signals += 1 # register if buy or sell signal is broken @@ -261,6 +262,11 @@ class LookaheadAnalysis: return None if "force_exit" in result_row['exit_reason']: logger.info("found force-exit, skipping this one to avoid a false-positive.") + + # just to keep the IDs of both full, entry and exit varholders the same + # to achieve a better debugging experience + self.entry_varHolders.append(VarHolder()) + self.exit_varHolders.append(VarHolder()) continue self.analyze_row(idx, result_row) From 1ab357dc32b07ed933019810007436f07351d11f Mon Sep 17 00:00:00 2001 From: hippocritical Date: Sun, 23 Jul 2023 15:29:25 +0200 Subject: [PATCH 013/116] added mentioning which pair + timerange + idx is biased for visibility and debugging purposes --- freqtrade/optimize/lookahead_analysis.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/lookahead_analysis.py b/freqtrade/optimize/lookahead_analysis.py index e87c3c7de..af3988066 100755 --- a/freqtrade/optimize/lookahead_analysis.py +++ b/freqtrade/optimize/lookahead_analysis.py @@ -141,7 +141,7 @@ class LookaheadAnalysis: shutil.rmtree(path_to_current_identifier) prepare_data_config = deepcopy(self.local_config) - prepare_data_config['timerange'] = (str(self.dt_to_timestamp(varholder.from_dt)) + "-" + + prepare_data_config['timerange'] = (str(self.dt_to_timestamp(varholder.from_dt)) + " - " + str(self.dt_to_timestamp(varholder.to_dt))) prepare_data_config['exchange']['pair_whitelist'] = pairs_to_load @@ -209,13 +209,16 @@ class LookaheadAnalysis: # fill entry_varHolder and exit_varHolder self.fill_entry_and_exit_varHolders(result_row) + # this will trigger a logger-message + buy_or_sell_biased: bool = False + # register if buy signal is broken if not self.report_signal( self.entry_varHolders[idx].result, "open_date", self.entry_varHolders[idx].compared_dt): - # logger.info(f"found lookahead-bias in trade {self.entry_varHolders[idx][]} {idx}") self.current_analysis.false_entry_signals += 1 + buy_or_sell_biased = True # register if buy or sell signal is broken if not self.report_signal( @@ -223,6 +226,13 @@ class LookaheadAnalysis: "close_date", self.exit_varHolders[idx].compared_dt): self.current_analysis.false_exit_signals += 1 + buy_or_sell_biased = True + + if buy_or_sell_biased: + logger.info(f"found lookahead-bias in trade " + f"pair: {result_row['pair']}, " + f"timerange:{result_row['open_date']}-{result_row['close_date']}, " + f"idx: {idx}") # check if the indicators themselves contain biased data self.analyze_indicators(self.full_varHolder, self.entry_varHolders[idx], result_row['pair']) @@ -261,7 +271,9 @@ class LookaheadAnalysis: f"Exiting this lookahead-analysis") return None if "force_exit" in result_row['exit_reason']: - logger.info("found force-exit, skipping this one to avoid a false-positive.") + logger.info("found force-exit in pair: {result_row['pair']}, " + f"timerange:{result_row['open_date']}-{result_row['close_date']}, " + f"idx: {idx}, skipping this one to avoid a false-positive.") # just to keep the IDs of both full, entry and exit varholders the same # to achieve a better debugging experience From ad428aa9b08d9ecaf33ef7594093cb6cc31d7e44 Mon Sep 17 00:00:00 2001 From: hippocritical Date: Sun, 23 Jul 2023 19:50:12 +0200 Subject: [PATCH 014/116] added stake_amount to a fixed 10k value. In a combination with a wallet size of 1 billion it should never be able to run out of money avoiding false-positives of some users who just wanted to test a strategy without actually checking how the stake_amount-variable should be used in combination with the strategy-function custom_stake_amount reason: some strategies demand a custom_stake_amount of 1$ demanding a very large wallet-size (which already was set previously) Others start with 100% of a slot size and subdivide the base-orders and safety-orders down to finish at 100% of a slot-size and use unlimited stake_amount. Edited docs to reflect that change too --- docs/lookahead-analysis.md | 3 +++ freqtrade/optimize/lookahead_analysis_helpers.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/docs/lookahead-analysis.md b/docs/lookahead-analysis.md index 9d57de779..c6eaa3e3c 100644 --- a/docs/lookahead-analysis.md +++ b/docs/lookahead-analysis.md @@ -22,6 +22,9 @@ It also supports the lookahead-analysis of freqai strategies. - `--cache` is forced to "none". - `--max-open-trades` is forced to be at least equal to the number of pairs. - `--dry-run-wallet` is forced to be basically infinite. +- `--stake-amount` is forced to be 10 k. + +Those are set to avoid users accidentally generating false positives. ## Lookahead-analysis command reference diff --git a/freqtrade/optimize/lookahead_analysis_helpers.py b/freqtrade/optimize/lookahead_analysis_helpers.py index 702eee774..7b1158fc8 100644 --- a/freqtrade/optimize/lookahead_analysis_helpers.py +++ b/freqtrade/optimize/lookahead_analysis_helpers.py @@ -136,6 +136,12 @@ class LookaheadAnalysisSubFunctions: logger.info('Dry run wallet was not set to 1 billion, pushing it up there ' 'just to avoid false positives') config['dry_run_wallet'] = min_dry_run_wallet + # fix stake_amount to 10k. + # in a combination with a wallet size of 1 billion it should always be able to trade + # no matter if they use custom_stake_amount as a small percentage of wallet size + # or fixate custom_stake_amount to a certain value. + logger.info('fixing stake_amount to 10.000') + config['stake_amount'] = 10000 # enforce cache to be 'none', shift it to 'none' if not already # (since the default value is 'day') From e4b488cb8472fdb834dd03d025902313f84d2f44 Mon Sep 17 00:00:00 2001 From: hippocritical Date: Sun, 23 Jul 2023 20:05:29 +0200 Subject: [PATCH 015/116] added stake_amount to a fixed 10k value. In a combination with a wallet size of 1 billion it should never be able to run out of money avoiding false-positives of some users who just wanted to test a strategy without actually checking how the stake_amount-variable should be used in combination with the strategy-function custom_stake_amount. reason: some strategies demand a custom_stake_amount of 1$ demanding a very large wallet-size (which already was set previously) Others start with 100% of a slot size and subdivide the base-orders and safety-orders down to finish at 100% of a slot-size and use unlimited stake_amount. Edited docs to reflect that change. --- docs/lookahead-analysis.md | 4 ++-- freqtrade/optimize/lookahead_analysis_helpers.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/lookahead-analysis.md b/docs/lookahead-analysis.md index c6eaa3e3c..e998b1b77 100644 --- a/docs/lookahead-analysis.md +++ b/docs/lookahead-analysis.md @@ -21,8 +21,8 @@ It also supports the lookahead-analysis of freqai strategies. - `--cache` is forced to "none". - `--max-open-trades` is forced to be at least equal to the number of pairs. -- `--dry-run-wallet` is forced to be basically infinite. -- `--stake-amount` is forced to be 10 k. +- `--dry-run-wallet` is forced to be basically infinite (1 billion). +- `--stake-amount` is forced to be a static 10000 (10k). Those are set to avoid users accidentally generating false positives. diff --git a/freqtrade/optimize/lookahead_analysis_helpers.py b/freqtrade/optimize/lookahead_analysis_helpers.py index 7b1158fc8..1fd4706f5 100644 --- a/freqtrade/optimize/lookahead_analysis_helpers.py +++ b/freqtrade/optimize/lookahead_analysis_helpers.py @@ -136,11 +136,12 @@ class LookaheadAnalysisSubFunctions: logger.info('Dry run wallet was not set to 1 billion, pushing it up there ' 'just to avoid false positives') config['dry_run_wallet'] = min_dry_run_wallet + # fix stake_amount to 10k. # in a combination with a wallet size of 1 billion it should always be able to trade # no matter if they use custom_stake_amount as a small percentage of wallet size # or fixate custom_stake_amount to a certain value. - logger.info('fixing stake_amount to 10.000') + logger.info('fixing stake_amount to 10k') config['stake_amount'] = 10000 # enforce cache to be 'none', shift it to 'none' if not already From 5b8800ee1819a20f12b1c0c42865effd535e5978 Mon Sep 17 00:00:00 2001 From: hippocritical Date: Sun, 23 Jul 2023 20:20:15 +0200 Subject: [PATCH 016/116] didnt intend to change the timerange itself, but the logger-output of the timerange --- freqtrade/optimize/lookahead_analysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/lookahead_analysis.py b/freqtrade/optimize/lookahead_analysis.py index af3988066..f363ae196 100755 --- a/freqtrade/optimize/lookahead_analysis.py +++ b/freqtrade/optimize/lookahead_analysis.py @@ -141,7 +141,7 @@ class LookaheadAnalysis: shutil.rmtree(path_to_current_identifier) prepare_data_config = deepcopy(self.local_config) - prepare_data_config['timerange'] = (str(self.dt_to_timestamp(varholder.from_dt)) + " - " + + prepare_data_config['timerange'] = (str(self.dt_to_timestamp(varholder.from_dt)) + "-" + str(self.dt_to_timestamp(varholder.to_dt))) prepare_data_config['exchange']['pair_whitelist'] = pairs_to_load @@ -231,7 +231,7 @@ class LookaheadAnalysis: if buy_or_sell_biased: logger.info(f"found lookahead-bias in trade " f"pair: {result_row['pair']}, " - f"timerange:{result_row['open_date']}-{result_row['close_date']}, " + f"timerange:{result_row['open_date']} - {result_row['close_date']}, " f"idx: {idx}") # check if the indicators themselves contain biased data From ac85c3527b2bc28a0fce5b8e4f27c4fdd6b82a50 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Aug 2023 16:21:12 +0000 Subject: [PATCH 017/116] Bump ccxt from 4.0.47 to 4.0.48 Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.0.47 to 4.0.48. - [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/4.0.47...4.0.48) --- 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 dd079329f..45cecbed3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ numpy==1.24.3; python_version <= '3.8' pandas==2.0.3 pandas-ta==0.3.14b -ccxt==4.0.47 +ccxt==4.0.48 cryptography==41.0.3; platform_machine != 'armv7l' cryptography==40.0.1; platform_machine == 'armv7l' aiohttp==3.8.5 From 2b7deb147d604b7549db8c63a3b7642c53af1811 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Aug 2023 17:43:11 +0000 Subject: [PATCH 018/116] Bump jsonschema from 4.18.4 to 4.18.5 Bumps [jsonschema](https://github.com/python-jsonschema/jsonschema) from 4.18.4 to 4.18.5. - [Release notes](https://github.com/python-jsonschema/jsonschema/releases) - [Changelog](https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst) - [Commits](https://github.com/python-jsonschema/jsonschema/compare/v4.18.4...v4.18.5) --- updated-dependencies: - dependency-name: jsonschema 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 dd079329f..7d92f3dea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ arrow==1.2.3 cachetools==5.3.1 requests==2.31.0 urllib3==2.0.4 -jsonschema==4.18.4 +jsonschema==4.18.5 TA-Lib==0.4.27 technical==1.4.0 tabulate==0.9.0 From 0e63335d2e8ff1cdd839d3e54d869b53de957223 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Aug 2023 20:01:31 +0200 Subject: [PATCH 019/116] Remove bitvavo temp. workaround --- tests/exchange/test_ccxt_compat.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index c57e32633..51d016d11 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -544,8 +544,6 @@ class TestCCXTExchange: if exchangename in ('bittrex'): # For some weired reason, this test returns random lengths for bittrex. pytest.skip("Exchange doesn't provide stable ohlcv history") - if exchangename in ('bitvavo'): - pytest.skip("Exchange Downtime ") if not exc._ft_has['ohlcv_has_history']: pytest.skip("Exchange does not support candle history") From 53c0d30f3627b7bd20641762227cf818972a6e02 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Aug 2023 20:04:41 +0200 Subject: [PATCH 020/116] Update test for new kucoin behavior related: https://github.com/ccxt/ccxt/pull/18745 --- tests/exchange/test_ccxt_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 51d016d11..0f8c43876 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -391,7 +391,7 @@ class TestCCXTExchange: assert po['id'] is not None if len(order.keys()) < 5: # Kucoin case - assert po['status'] == 'closed' + assert po['status'] is None continue assert po['timestamp'] == 1674493798550 assert isinstance(po['datetime'], str) From 2f95c44777c2f4a6d6be50e23d54922b1a6236f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 31 Jul 2023 21:15:33 +0200 Subject: [PATCH 021/116] Add "notes" to backtest result output --- freqtrade/data/btanalysis.py | 16 ++++++++++++++++ freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/types/backtest_result_type.py | 3 ++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 206588d37..edac74d51 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -175,6 +175,21 @@ def _get_backtest_files(dirname: Path) -> List[Path]: return list(reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json')))) +def get_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]: + """ + Get backtest result read from metadata file + """ + return [ + { + 'filename': filename.stem, + 'strategy': s, + 'notes': v.get('notes', ''), + 'run_id': v['run_id'], + 'backtest_start_time': v['backtest_start_time'], + } for s, v in load_backtest_metadata(filename).items() + ] + + def get_backtest_resultlist(dirname: Path) -> List[BacktestHistoryEntryType]: """ Get list of backtest results read from metadata files @@ -184,6 +199,7 @@ def get_backtest_resultlist(dirname: Path) -> List[BacktestHistoryEntryType]: 'filename': filename.stem, 'strategy': s, 'run_id': v['run_id'], + 'notes': v.get('notes', ''), 'backtest_start_time': v['backtest_start_time'], } for filename in _get_backtest_files(dirname) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index bd405d22b..f96e586bb 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -526,6 +526,7 @@ class BacktestHistoryEntry(BaseModel): strategy: str run_id: str backtest_start_time: int + notes: Optional[str] = '' class SysInfo(BaseModel): diff --git a/freqtrade/types/backtest_result_type.py b/freqtrade/types/backtest_result_type.py index bc53097ab..1b66e6b1c 100644 --- a/freqtrade/types/backtest_result_type.py +++ b/freqtrade/types/backtest_result_type.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from typing_extensions import TypedDict @@ -6,6 +6,7 @@ from typing_extensions import TypedDict class BacktestMetadataType(TypedDict): run_id: str backtest_start_time: int + notes: Optional[str] class BacktestResultType(TypedDict): From 78972604d0b374a0667111bf847114bf7531b4ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 31 Jul 2023 21:16:25 +0200 Subject: [PATCH 022/116] Allow metadata file updating --- freqtrade/data/btanalysis.py | 17 +++++++++++++- freqtrade/rpc/api_server/api_backtest.py | 30 ++++++++++++++++++++---- freqtrade/rpc/api_server/api_schemas.py | 5 ++++ 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index edac74d51..96ab4927e 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -12,7 +12,7 @@ import pandas as pd from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf from freqtrade.exceptions import OperationalException -from freqtrade.misc import json_load +from freqtrade.misc import file_dump_json, json_load from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.types import BacktestHistoryEntryType, BacktestResultType @@ -219,6 +219,21 @@ def delete_backtest_result(file_abs: Path): file_abs_meta.unlink() +def update_backtest_metadata(filename: Path, strategy: str, content: Dict[str, Any]): + """ + Updates backtest metadata file with new content. + :raises: ValueError if metadata file does not exist, or strategy is not in this file. + """ + metadata = load_backtest_metadata(filename) + if not metadata: + raise ValueError("File does not exist.") + if strategy not in metadata: + raise ValueError("Strategy not in metadata.") + metadata[strategy].update(content) + # Write data again. + file_dump_json(get_backtest_metadata_filename(filename), metadata) + + def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str], min_backtest_date: Optional[datetime] = None) -> Dict[str, Any]: """ diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index 6d3174a5a..3bfad2f56 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -10,14 +10,15 @@ from fastapi.exceptions import HTTPException from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.constants import Config -from freqtrade.data.btanalysis import (delete_backtest_result, get_backtest_resultlist, - load_and_merge_backtest_result) +from freqtrade.data.btanalysis import (delete_backtest_result, get_backtest_result, + get_backtest_resultlist, load_and_merge_backtest_result, + update_backtest_metadata) from freqtrade.enums import BacktestState from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange.common import remove_exchange_credentials from freqtrade.misc import deep_merge_dicts, is_file_in_dir -from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest, - BacktestResponse) +from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestMetadataUpdate, + BacktestRequest, BacktestResponse) from freqtrade.rpc.api_server.deps import get_config from freqtrade.rpc.api_server.webserver_bgwork import ApiBG from freqtrade.rpc.rpc import RPCException @@ -281,3 +282,24 @@ def api_delete_backtest_history_entry(file: str, config=Depends(get_config)): delete_backtest_result(file_abs) return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results') + + +@router.patch('/backtest/history/{file}', response_model=List[BacktestHistoryEntry], + tags=['webserver', 'backtest']) +def api_update_backtest_history_entry(file: str, body: BacktestMetadataUpdate, + config=Depends(get_config)): + # Get backtest result history, read from metadata files + bt_results_base: Path = config['user_data_dir'] / 'backtest_results' + file_abs = (bt_results_base / file).with_suffix('.json') + # Ensure file is in backtest_results directory + if not is_file_in_dir(file_abs, bt_results_base): + raise HTTPException(status_code=404, detail="File not found.") + content = { + 'notes': body.notes + } + try: + update_backtest_metadata(file_abs, body.strategy, content) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + return get_backtest_result(file_abs) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index f96e586bb..ca39b11fd 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -529,6 +529,11 @@ class BacktestHistoryEntry(BaseModel): notes: Optional[str] = '' +class BacktestMetadataUpdate(BaseModel): + strategy: str + notes: Optional[str] + + class SysInfo(BaseModel): cpu_pct: List[float] ram_pct: float From 0d71a74d8aa726f20d6a0bcdae0e93de336cd5f0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Aug 2023 06:30:23 +0200 Subject: [PATCH 023/116] Bump api version to 2.32 --- freqtrade/rpc/api_server/api_v1.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 3e5d55f71..bc0c88fe4 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -50,7 +50,8 @@ logger = logging.getLogger(__name__) # 2.29: Add /exchanges endpoint # 2.30: new /pairlists endpoint # 2.31: new /backtest/history/ delete endpoint -API_VERSION = 2.31 +# 2.32: new /backtest/history/ patch endpoint +API_VERSION = 2.32 # Public API, requires no auth. router_public = APIRouter() From 51879ffd2caaa6c53d0a77e0b0e0e59d9b8301ff Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Aug 2023 06:57:02 +0200 Subject: [PATCH 024/116] move Notes to be a "API only" type --- freqtrade/types/backtest_result_type.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/types/backtest_result_type.py b/freqtrade/types/backtest_result_type.py index 1b66e6b1c..1043899f7 100644 --- a/freqtrade/types/backtest_result_type.py +++ b/freqtrade/types/backtest_result_type.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List from typing_extensions import TypedDict @@ -6,7 +6,6 @@ from typing_extensions import TypedDict class BacktestMetadataType(TypedDict): run_id: str backtest_start_time: int - notes: Optional[str] class BacktestResultType(TypedDict): @@ -26,3 +25,4 @@ def get_BacktestResultType_default() -> BacktestResultType: class BacktestHistoryEntryType(BacktestMetadataType): filename: str strategy: str + notes: str From 36b84241b159e4ef88b3bfd220341ccb007024cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Aug 2023 06:28:28 +0200 Subject: [PATCH 025/116] Don't allow null as notes --- freqtrade/rpc/api_server/api_schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index ca39b11fd..f1ac9db54 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -531,7 +531,7 @@ class BacktestHistoryEntry(BaseModel): class BacktestMetadataUpdate(BaseModel): strategy: str - notes: Optional[str] + notes: str = '' class SysInfo(BaseModel): From 23a2b9599463866a004daf6a1d641fccaf75a33c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Aug 2023 06:39:27 +0200 Subject: [PATCH 026/116] Add test for updating metadata --- tests/rpc/test_rpc_apiserver.py | 82 ++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index abbaa421e..095e4ac3f 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -10,6 +10,7 @@ from unittest.mock import ANY, MagicMock, PropertyMock import pandas as pd import pytest +import rapidjson import uvicorn from fastapi import FastAPI, WebSocketDisconnect from fastapi.exceptions import HTTPException @@ -79,6 +80,14 @@ def client_post(client: TestClient, url, data={}): 'content-type': 'application/json' }) +def client_patch(client: TestClient, url, data={}): + + return client.patch(url, + json=data, + headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS), + 'Origin': 'http://example.com', + 'content-type': 'application/json' + }) def client_get(client: TestClient, url): # Add fake Origin to ensure CORS kicks in @@ -1763,7 +1772,7 @@ def test_api_pairlists_evaluate(botclient, tmpdir, mocker): rc = client_get(client, f"{BASE_URI}/pairlists/evaluate/{job_id}") assert_response(rc) response = rc.json() - assert response['result']['whitelist'] == ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC',] + assert response['result']['whitelist'] == ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'] assert response['result']['length'] == 4 # Restart with additional filter, reducing the list to 2 @@ -2023,7 +2032,7 @@ def test_api_backtest_history(botclient, mocker, testdatadir): assert result2['backtest_result']['strategy'][strategy] -def test_api_delete_backtest_history_entry(botclient, mocker, tmp_path: Path): +def test_api_delete_backtest_history_entry(botclient, tmp_path: Path): ftbot, client = botclient # Create a temporary directory and file @@ -2051,6 +2060,75 @@ def test_api_delete_backtest_history_entry(botclient, mocker, tmp_path: Path): assert not meta_path.exists() +def test_api_patch_backtest_history_entry(botclient, tmp_path: Path): + ftbot, client = botclient + + # Create a temporary directory and file + bt_results_base = tmp_path / "backtest_results" + bt_results_base.mkdir() + file_path = bt_results_base / "test.json" + file_path.touch() + meta_path = file_path.with_suffix('.meta.json') + with meta_path.open('w') as metafile: + rapidjson.dump({ + CURRENT_TEST_STRATEGY: { + "run_id": "6e542efc8d5e62cef6e5be0ffbc29be81a6e751d", + "backtest_start_time": 1690176003} + }, metafile) + + def read_metadata(): + with meta_path.open('r') as metafile: + return rapidjson.load(metafile) + + rc = client_patch(client, f"{BASE_URI}/backtest/history/randomFile.json") + assert_response(rc, 503) + + ftbot.config['user_data_dir'] = tmp_path + ftbot.config['runmode'] = RunMode.WEBSERVER + + rc = client_patch(client, f"{BASE_URI}/backtest/history/randomFile.json", { + "strategy": CURRENT_TEST_STRATEGY, + }) + assert rc.status_code == 404 + + # Nonexisting strategy + rc = client_patch(client, f"{BASE_URI}/backtest/history/{file_path.name}", { + "strategy": f"{CURRENT_TEST_STRATEGY}xxx", + }) + assert rc.status_code == 400 + assert rc.json()['detail'] == 'Strategy not in metadata.' + + # no Notes + rc = client_patch(client, f"{BASE_URI}/backtest/history/{file_path.name}", { + "strategy": CURRENT_TEST_STRATEGY, + }) + assert rc.status_code == 200 + res = rc.json() + assert isinstance(res, list) + assert len(res) == 1 + assert res[0]['strategy'] == CURRENT_TEST_STRATEGY + assert res[0]['notes'] == '' + + fileres = read_metadata() + assert fileres[CURRENT_TEST_STRATEGY]['run_id'] == res[0]['run_id'] + assert fileres[CURRENT_TEST_STRATEGY]['notes'] == '' + + rc = client_patch(client, f"{BASE_URI}/backtest/history/{file_path.name}", { + "strategy": CURRENT_TEST_STRATEGY, + "notes": "FooBar", + }) + assert rc.status_code == 200 + res = rc.json() + assert isinstance(res, list) + assert len(res) == 1 + assert res[0]['strategy'] == CURRENT_TEST_STRATEGY + assert res[0]['notes'] == 'FooBar' + + fileres = read_metadata() + assert fileres[CURRENT_TEST_STRATEGY]['run_id'] == res[0]['run_id'] + assert fileres[CURRENT_TEST_STRATEGY]['notes'] == 'FooBar' + + def test_health(botclient): ftbot, client = botclient From 6d6111864e3f74e3d32eebe90f703c568af17970 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Aug 2023 06:42:15 +0200 Subject: [PATCH 027/116] Test also backtest result list --- tests/rpc/test_rpc_apiserver.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 095e4ac3f..0c9c964cf 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -80,6 +80,7 @@ def client_post(client: TestClient, url, data={}): 'content-type': 'application/json' }) + def client_patch(client: TestClient, url, data={}): return client.patch(url, @@ -89,6 +90,7 @@ def client_patch(client: TestClient, url, data={}): 'content-type': 'application/json' }) + def client_get(client: TestClient, url): # Add fake Origin to ensure CORS kicks in return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS), @@ -2019,6 +2021,7 @@ def test_api_backtest_history(botclient, mocker, testdatadir): assert len(result) == 3 fn = result[0]['filename'] assert fn == "backtest-result_multistrat" + assert result[0]['notes'] == '' strategy = result[0]['strategy'] rc = client_get(client, f"{BASE_URI}/backtest/history/result?filename={fn}&strategy={strategy}") assert_response(rc) From 81cd2419548310c7816567bffd067610a5d35dc5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Aug 2023 07:05:57 +0200 Subject: [PATCH 028/116] Update API backtest to return proper metadata --- freqtrade/optimize/optimize_reports/bt_storage.py | 4 +++- freqtrade/rpc/api_server/api_backtest.py | 9 ++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/optimize_reports/bt_storage.py b/freqtrade/optimize/optimize_reports/bt_storage.py index 71c6dc130..6b50412b3 100644 --- a/freqtrade/optimize/optimize_reports/bt_storage.py +++ b/freqtrade/optimize/optimize_reports/bt_storage.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) def store_backtest_stats( - recordfilename: Path, stats: BacktestResultType, dtappendix: str) -> None: + recordfilename: Path, stats: BacktestResultType, dtappendix: str) -> Path: """ Stores backtest results :param recordfilename: Path object, which can either be a filename or a directory. @@ -41,6 +41,8 @@ def store_backtest_stats( latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN) file_dump_json(latest_filename, {'latest_backtest': str(filename.name)}) + return filename + def _store_backtest_analysis_data( recordfilename: Path, data: Dict[str, Dict], diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index 3bfad2f56..f387a9ac8 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -75,10 +75,11 @@ def __run_backtest_bg(btconfig: Config): ApiBG.bt['bt'].load_prior_backtest() ApiBG.bt['bt'].abort = False + strategy_name = strat.get_strategy_name() if (ApiBG.bt['bt'].results and - strat.get_strategy_name() in ApiBG.bt['bt'].results['strategy']): + strategy_name in ApiBG.bt['bt'].results['strategy']): # When previous result hash matches - reuse that result and skip backtesting. - logger.info(f'Reusing result of previous backtest for {strat.get_strategy_name()}') + logger.info(f'Reusing result of previous backtest for {strategy_name}') else: min_date, max_date = ApiBG.bt['bt'].backtest_one_strategy( strat, ApiBG.bt['data'], ApiBG.bt['timerange']) @@ -88,10 +89,12 @@ def __run_backtest_bg(btconfig: Config): min_date=min_date, max_date=max_date) if btconfig.get('export', 'none') == 'trades': - store_backtest_stats( + fn = store_backtest_stats( btconfig['exportfilename'], ApiBG.bt['bt'].results, datetime.now().strftime("%Y-%m-%d_%H-%M-%S") ) + ApiBG.bt['bt'].results['metadata'][strategy_name]['filename'] = str(fn.name) + ApiBG.bt['bt'].results['metadata'][strategy_name]['strategy'] = strategy_name logger.info("Backtest finished.") From 836d7b885a0b91aabbce30a80833236fdafac9b0 Mon Sep 17 00:00:00 2001 From: yinon Date: Fri, 4 Aug 2023 12:50:01 +0000 Subject: [PATCH 029/116] pytorch - trainer - set default usage of n_epochs instead of max_iters --- freqtrade/freqai/torch/PyTorchModelTrainer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index 7a8857994..dc34e8907 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -50,8 +50,8 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): self.criterion = criterion self.model_meta_data = model_meta_data self.device = device - self.max_iters: int = kwargs.get("max_iters", 100) - self.n_epochs: Optional[int] = kwargs.get("n_epochs", None) + self.max_iters: int = kwargs.get("max_iters", None) + self.n_epochs: Optional[int] = kwargs.get("n_epochs", 10) if not self.max_iters and not self.n_epochs: raise Exception("Either `max_iters` or `n_epochs` should be set.") From 777d25192c6cdcf642929c916df2b9c5432422f5 Mon Sep 17 00:00:00 2001 From: yinon Date: Fri, 4 Aug 2023 12:51:42 +0000 Subject: [PATCH 030/116] pytorch - bugfix - explicitly assign tensor to var as .to() is not inplace operation --- freqtrade/freqai/torch/PyTorchModelTrainer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index dc34e8907..e7c4d53be 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -113,8 +113,8 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): self.model.eval() for _, batch_data in enumerate(data_loader_dictionary[split]): xb, yb = batch_data - xb.to(self.device) - yb.to(self.device) + xb = xb.to(self.device) + yb = yb.to(self.device) yb_pred = self.model(xb) loss = self.criterion(yb_pred, yb) From d17bf6350d10ed3533ac264a7de24faaac572090 Mon Sep 17 00:00:00 2001 From: yinon Date: Fri, 4 Aug 2023 12:52:55 +0000 Subject: [PATCH 031/116] pytorch - trainer - revert load changes --- freqtrade/freqai/torch/PyTorchModelTrainer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index e7c4d53be..2b0090c78 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -181,8 +181,8 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): "pytrainer": self }, path) - def load(self, path: Path, device: Optional[str] = None): - checkpoint = torch.load(path, map_location=device) + def load(self, path: Path): + checkpoint = torch.load(path) return self.load_from_checkpoint(checkpoint) def load_from_checkpoint(self, checkpoint: Dict): From a3c6904fbcca31642aee4fa4b69fce293ee02010 Mon Sep 17 00:00:00 2001 From: yinon Date: Fri, 4 Aug 2023 13:45:21 +0000 Subject: [PATCH 032/116] pytorch - naming refactor - max_iters to n_steps --- .../prediction_models/PyTorchMLPClassifier.py | 2 +- .../prediction_models/PyTorchMLPRegressor.py | 2 +- .../PyTorchTransformerRegressor.py | 2 +- freqtrade/freqai/torch/PyTorchModelTrainer.py | 14 +++++++------- tests/freqai/conftest.py | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index ca333d9cf..9aabdf7ad 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -26,7 +26,7 @@ class PyTorchMLPClassifier(BasePyTorchClassifier): "model_training_parameters" : { "learning_rate": 3e-4, "trainer_kwargs": { - "max_iters": 5000, + "n_steps": 5000, "batch_size": 64, "n_epochs": null, }, diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 42fddf8ff..dc8dc4b61 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -27,7 +27,7 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): "model_training_parameters" : { "learning_rate": 3e-4, "trainer_kwargs": { - "max_iters": 5000, + "n_steps": 5000, "batch_size": 64, "n_epochs": null, }, diff --git a/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py b/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py index 32663c86b..846d6df2e 100644 --- a/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py @@ -30,7 +30,7 @@ class PyTorchTransformerRegressor(BasePyTorchRegressor): "model_training_parameters" : { "learning_rate": 3e-4, "trainer_kwargs": { - "max_iters": 5000, + "n_steps": 5000, "batch_size": 64, "n_epochs": null }, diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index 2b0090c78..44f7dec4e 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -39,7 +39,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): 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: used to calculate n_epochs. The number of training iterations to run. + :param n_steps: used to calculate n_epochs. The number of training iterations to run. iteration here refers to the number of times optimizer.step() is called. ignored if n_epochs is set. :param n_epochs: The maximum number batches to use for evaluation. @@ -50,10 +50,10 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): self.criterion = criterion self.model_meta_data = model_meta_data self.device = device - self.max_iters: int = kwargs.get("max_iters", None) + self.n_steps: int = kwargs.get("n_steps", None) self.n_epochs: Optional[int] = kwargs.get("n_epochs", 10) - if not self.max_iters and not self.n_epochs: - raise Exception("Either `max_iters` or `n_epochs` should be set.") + if not self.n_steps and not self.n_epochs: + raise Exception("Either `n_steps` or `n_epochs` should be set.") self.batch_size: int = kwargs.get("batch_size", 64) self.data_convertor = data_convertor @@ -82,7 +82,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): n_epochs = self.n_epochs or self.calc_n_epochs( n_obs=n_obs, batch_size=self.batch_size, - n_iters=self.max_iters, + n_iters=self.n_steps, ) batch_counter = 0 @@ -153,7 +153,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): 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, + the motivation here is that `n_steps` is easier to optimize and keep stable, across different n_obs - the number of data points. """ @@ -162,7 +162,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): if n_epochs <= 10: logger.warning( f"Setting low n_epochs: {n_epochs}. " - f"Please consider increasing `max_iters` hyper-parameter." + f"Please consider increasing `n_steps` hyper-parameter." ) return n_epochs diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index 96716e83f..9c7a950e7 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -97,9 +97,9 @@ def mock_pytorch_mlp_model_training_parameters() -> Dict[str, Any]: return { "learning_rate": 3e-4, "trainer_kwargs": { - "max_iters": 1, + "n_steps": None, "batch_size": 64, - "n_epochs": None, + "n_epochs": 1, }, "model_kwargs": { "hidden_dim": 32, From 9f69a45afd5ff93c74bb4fab88cb03b3cefed600 Mon Sep 17 00:00:00 2001 From: yinon Date: Fri, 4 Aug 2023 13:46:30 +0000 Subject: [PATCH 033/116] pytorch - documentation update --- docs/freqai-parameter-table.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 5e60d2a07..de0b666ca 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -100,12 +100,12 @@ Mandatory parameters are marked as **Required** and have to be set in one of the #### 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`. +| Parameter | Description | +|----------------------|-------------| +| | **Model training parameters within the `freqai.model_training_parameters.model_kwargs` sub dictionary** +| `n_epochs` | The `n_epochs` parameter is a crucial setting in the PyTorch training loop that determines the number of times the entire training dataset will be used to update the model's parameters. An epoch represents one full pass through the entire training dataset.
**Datatype:** int.
Default: `10`. +| `n_steps` | An alternative way of setting `n_epochs` - the number of training iterations to run. Iteration here refer to the number of times we call `optimizer.step()`. a simplified version of the function:

n_epochs = n_steps / (n_obs / batch_size)

The motivation here is that `n_steps` is easier to optimize and keep stable across different n_obs - the number of data points.

**Datatype:** int. optional.
Default: `None`. +| `batch_size` | The size of the batches to use during training..
**Datatype:** int.
Default: `64`. ### Additional parameters From 23d2bad2a08e09fdd8ec1f02f1af318a05e510a9 Mon Sep 17 00:00:00 2001 From: yinon Date: Fri, 4 Aug 2023 14:33:59 +0000 Subject: [PATCH 034/116] pytorch - set n_steps type as optional --- freqtrade/freqai/torch/PyTorchModelTrainer.py | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index 44f7dec4e..371a953e7 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -50,9 +50,9 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): self.criterion = criterion self.model_meta_data = model_meta_data self.device = device - self.n_steps: int = kwargs.get("n_steps", None) self.n_epochs: Optional[int] = kwargs.get("n_epochs", 10) - if not self.n_steps and not self.n_epochs: + self.n_steps: Optional[int] = kwargs.get("n_steps", None) + if self.n_steps is None and not self.n_epochs: raise Exception("Either `n_steps` or `n_epochs` should be set.") self.batch_size: int = kwargs.get("batch_size", 64) @@ -79,12 +79,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary, splits) n_obs = len(data_dictionary["train_features"]) - n_epochs = self.n_epochs or self.calc_n_epochs( - n_obs=n_obs, - batch_size=self.batch_size, - n_iters=self.n_steps, - ) - + n_epochs = self.n_epochs or self.calc_n_epochs(n_obs=n_obs) batch_counter = 0 for _ in range(n_epochs): for _, batch_data in enumerate(data_loaders_dictionary["train"]): @@ -147,8 +142,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): return data_loader_dictionary - @staticmethod - def calc_n_epochs(n_obs: int, batch_size: int, n_iters: int) -> int: + def calc_n_epochs(self, n_obs: int) -> int: """ Calculates the number of epochs required to reach the maximum number of iterations specified in the model training parameters. @@ -156,9 +150,9 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): the motivation here is that `n_steps` is easier to optimize and keep stable, across different n_obs - the number of data points. """ - - n_batches = n_obs // batch_size - n_epochs = min(n_iters // n_batches, 1) + assert isinstance(self.n_steps, int), "Either `n_steps` or `n_epochs` should be set." + n_batches = n_obs // self.batch_size + n_epochs = min(self.n_steps // n_batches, 1) if n_epochs <= 10: logger.warning( f"Setting low n_epochs: {n_epochs}. " From bdf89efd113944f0b00ca6733b8acb905022471e Mon Sep 17 00:00:00 2001 From: yinon Date: Fri, 4 Aug 2023 14:42:28 +0000 Subject: [PATCH 035/116] pytorch - improve docs --- docs/freqai-parameter-table.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index de0b666ca..95687c7ab 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -100,12 +100,12 @@ Mandatory parameters are marked as **Required** and have to be set in one of the #### trainer_kwargs -| Parameter | Description | -|----------------------|-------------| -| | **Model training parameters within the `freqai.model_training_parameters.model_kwargs` sub dictionary** -| `n_epochs` | The `n_epochs` parameter is a crucial setting in the PyTorch training loop that determines the number of times the entire training dataset will be used to update the model's parameters. An epoch represents one full pass through the entire training dataset.
**Datatype:** int.
Default: `10`. -| `n_steps` | An alternative way of setting `n_epochs` - the number of training iterations to run. Iteration here refer to the number of times we call `optimizer.step()`. a simplified version of the function:

n_epochs = n_steps / (n_obs / batch_size)

The motivation here is that `n_steps` is easier to optimize and keep stable across different n_obs - the number of data points.

**Datatype:** int. optional.
Default: `None`. -| `batch_size` | The size of the batches to use during training..
**Datatype:** int.
Default: `64`. +| Parameter | Description | +|--------------|-------------| +| | **Model training parameters within the `freqai.model_training_parameters.model_kwargs` sub dictionary** +| `n_epochs` | The `n_epochs` parameter is a crucial setting in the PyTorch training loop that determines the number of times the entire training dataset will be used to update the model's parameters. An epoch represents one full pass through the entire training dataset. Overrides `n_steps`. Either `n_epochs` or `n_steps` must be set.

**Datatype:** int. optional.
Default: `10`. +| `n_steps` | An alternative way of setting `n_epochs` - the number of training iterations to run. Iteration here refer to the number of times we call `optimizer.step()`. Ignored if `n_epochs` is set. A simplified version of the function:

n_epochs = n_steps / (n_obs / batch_size)

The motivation here is that `n_steps` is easier to optimize and keep stable across different n_obs - the number of data points.

**Datatype:** int. optional.
Default: `None`. +| `batch_size` | The size of the batches to use during training.

**Datatype:** int.
Default: `64`. ### Additional parameters From 25602ceac35811e2d5377ccf8e08c84f1425b112 Mon Sep 17 00:00:00 2001 From: hippocritical Date: Sat, 5 Aug 2023 08:24:47 +0200 Subject: [PATCH 036/116] Added a fixed fee to 0.02 (any fixed value would suffice) since kucoin dynamically decides which pair gets which amount of fees and thereby producing false-positives upon verifying the entries/exits. Added a check for timerange being set. --- freqtrade/optimize/lookahead_analysis_helpers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/freqtrade/optimize/lookahead_analysis_helpers.py b/freqtrade/optimize/lookahead_analysis_helpers.py index 1fd4706f5..654ff93d2 100644 --- a/freqtrade/optimize/lookahead_analysis_helpers.py +++ b/freqtrade/optimize/lookahead_analysis_helpers.py @@ -137,6 +137,16 @@ class LookaheadAnalysisSubFunctions: 'just to avoid false positives') config['dry_run_wallet'] = min_dry_run_wallet + if 'fee' not in config or config['fee'] != 0.02: + logger.info('fee was not set to a fixed value of 0.02. ') + config['fee'] = 0.02 + + if 'timerange' not in config: + # setting a timerange is enforced here + raise OperationalException( + "Please set a timerange. " + "Usually a few months are enough depending on your needs and strategy." + ) # fix stake_amount to 10k. # in a combination with a wallet size of 1 billion it should always be able to trade # no matter if they use custom_stake_amount as a small percentage of wallet size From e174f9640d1038edfcd619aeba61bd03244986a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Aug 2023 11:14:19 +0200 Subject: [PATCH 037/116] Improve docker docs with some hints about windows usage --- docs/docker_quickstart.md | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 89f737d71..0c9e8001a 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -14,6 +14,9 @@ Start by downloading and installing Docker / Docker Desktop for your platform: Freqtrade documentation assumes the use of Docker desktop (or the docker compose plugin). While the docker-compose standalone installation still works, it will require changing all `docker compose` commands from `docker compose` to `docker-compose` to work (e.g. `docker compose up -d` will become `docker-compose up -d`). +??? Warning "Docker on windows" + If you just installed docker on a windows system, make sure to reboot your system, otherwise you might encounter unexplainable Problems related to network connectivity to docker containers. + ## Freqtrade with docker Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker compose file](https://github.com/freqtrade/freqtrade/blob/stable/docker-compose.yml) ready for usage. @@ -78,7 +81,7 @@ If you've selected to enable FreqUI in the `new-config` step, you will have freq You can now access the UI by typing localhost:8080 in your browser. -??? Note "UI Access on a remote servers" +??? Note "UI Access on a remote server" If you're running on a VPS, you should consider using either a ssh tunnel, or setup a VPN (openVPN, wireguard) to connect to your bot. This will ensure that freqUI is not directly exposed to the internet, which is not recommended for security reasons (freqUI does not support https out of the box). Setup of these tools is not part of this tutorial, however many good tutorials can be found on the internet. @@ -128,7 +131,7 @@ All freqtrade arguments will be available by running `docker compose run --rm fr !!! Note "`docker compose run --rm`" Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). -??? Note "Using docker without docker" +??? Note "Using docker without docker compose" "`docker compose run --rm`" will require a compose file to be provided. Some freqtrade commands that don't require authentication such as `list-pairs` can be run with "`docker run --rm`" instead. For example `docker run --rm freqtradeorg/freqtrade:stable list-pairs --exchange binance --quote BTC --print-json`. @@ -172,7 +175,7 @@ You can then run `docker compose build --pull` to build the docker image, and ru ### Plotting with docker -Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file. +Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your `docker-compose.yml` file. You can then use these commands as follows: ``` bash @@ -203,16 +206,20 @@ docker compose -f docker/docker-compose-jupyter.yml build --no-cache ### Docker on Windows -* Error: `"Timestamp for this request is outside of the recvWindow."` - * The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past. - To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so). - A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler. +* Error: `"Timestamp for this request is outside of the recvWindow."` + The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past. + To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so). + A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler. - ``` bash - taskkill /IM "Docker Desktop.exe" /F - wsl --shutdown - start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe" - ``` + ``` bash + taskkill /IM "Docker Desktop.exe" /F + wsl --shutdown + start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe" + ``` + +* Cannot connect to the API (Windows) + If you're on windows and just installed Docker (desktop), make sure to reboot your System. Docker can have problems with network connectivity without a restart. + You should obviously also make sure to have your [settings](#accessing-the-ui) accordingly. !!! Warning Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting. From 72d9e8a094333ce1204a9effb38ea78d76a61aa8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Aug 2023 11:31:01 +0200 Subject: [PATCH 038/116] Fix indentation of strategy-updater in utils --- docs/utils.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/utils.md b/docs/utils.md index 900856af4..65ab50b9e 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -967,7 +967,7 @@ Print trades with id 2 and 3 as json freqtrade show-trades --db-url sqlite:///tradesv3.sqlite --trade-ids 2 3 --print-json ``` -### Strategy-Updater +## Strategy-Updater Updates listed strategies or all strategies within the strategies folder to be v3 compliant. If the command runs without --strategy-list then all strategies inside the strategies folder will be converted. From 7d18261f58c9dc7be46a5d0d8ddd72f922a53f3b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Aug 2023 17:10:38 +0200 Subject: [PATCH 039/116] Improve FAQ wording --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 7b8cc2580..ecf4f45a9 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -20,7 +20,7 @@ Futures trading is supported for selected exchanges. Please refer to the [docume * When you work with your strategy & hyperopt file you should use a proper code editor like VSCode or PyCharm. A good code editor will provide syntax highlighting as well as line numbers, making it easy to find syntax errors (most likely pointed out by Freqtrade during startup). -## Freqtrade common issues +## Freqtrade common questions ### Can freqtrade open multiple positions on the same pair in parallel? From cd6fc1652ed8d3406e3905ef73f96e9ae658ed14 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Aug 2023 17:11:17 +0200 Subject: [PATCH 040/116] Add rate-limited wallets call before adjust_trade-Position calls closes #8998 --- freqtrade/freqtradebot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9d35aee0f..beca1f09c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -613,6 +613,8 @@ class FreqtradeBot(LoggingMixin): for trade in Trade.get_open_trades(): # If there is any open orders, wait for them to finish. if trade.open_order_id is None: + # Do a wallets update (will be ratelimited to once per hour) + self.wallets.update(False) try: self.check_and_call_adjust_trade_position(trade) except DependencyException as exception: From 813ace12c925dfeb975fa81620de53ce1e6f210a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Aug 2023 17:11:33 +0200 Subject: [PATCH 041/116] Explain behavior in case of deposits related to #8998 --- docs/faq.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index ecf4f45a9..0a3b152cb 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -78,6 +78,14 @@ Where possible (e.g. on binance), the use of the exchange's dedicated fee curren On binance, it's sufficient to have BNB in your account, and have "Pay fees in BNB" enabled in your profile. Your BNB balance will slowly decline (as it's used to pay fees) - but you'll no longer encounter dust (Freqtrade will include the fees in the profit calculations). Other exchanges don't offer such possibilities, where it's simply something you'll have to accept or move to a different exchange. +### I deposited more funds to the exchange, but my bot doesn't recognize this + +Freqtrade will update the exchange balance when necessary (Before placing an order). +RPC calls (Telegram's `/balance`, API calls to `/balance`) can trigger an update at max. once per hour. + +If `adjust_trade_position` is enabled (and the bot has open trades eligible for position adjustments) - then the wallets will be refreshed once per hour. +To force an immediate update, you can use `/reload_config` - which will restart the bot. + ### I want to use incomplete candles Freqtrade will not provide incomplete candles to strategies. Using incomplete candles will lead to repainting and consequently to strategies with "ghost" buys, which are impossible to both backtest, and verify after they happened. From 67ea6512ef3c4c44e946bafc8ef28b976c50e66f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 03:05:10 +0000 Subject: [PATCH 042/116] Bump ccxt from 4.0.48 to 4.0.50 Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.0.48 to 4.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/4.0.48...4.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 e271b7e3a..0c8aba5b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ numpy==1.24.3; python_version <= '3.8' pandas==2.0.3 pandas-ta==0.3.14b -ccxt==4.0.48 +ccxt==4.0.50 cryptography==41.0.3; platform_machine != 'armv7l' cryptography==40.0.1; platform_machine == 'armv7l' aiohttp==3.8.5 From 8c3a0cf6182936494f6c36c6959f6fc432068a99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 03:05:18 +0000 Subject: [PATCH 043/116] Bump orjson from 3.9.2 to 3.9.3 Bumps [orjson](https://github.com/ijl/orjson) from 3.9.2 to 3.9.3. - [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.9.2...3.9.3) --- 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 e271b7e3a..3fd4ce7de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,7 @@ py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.10 # Properly format api responses -orjson==3.9.2 +orjson==3.9.3 # Notify systemd sdnotify==0.3.2 From 8deedc31c58a915c45fd430a8f864a09f1322fea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 03:05:27 +0000 Subject: [PATCH 044/116] Bump fastapi from 0.100.1 to 0.101.0 Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.100.1 to 0.101.0. - [Release notes](https://github.com/tiangolo/fastapi/releases) - [Commits](https://github.com/tiangolo/fastapi/compare/0.100.1...0.101.0) --- updated-dependencies: - dependency-name: fastapi 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 e271b7e3a..835334cef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,7 +39,7 @@ orjson==3.9.2 sdnotify==0.3.2 # API Server -fastapi==0.100.1 +fastapi==0.101.0 pydantic==1.10.11 uvicorn==0.23.2 pyjwt==2.8.0 From ae3c3d81c1613ea5f3ef7594025e93d6c6bf6dfe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 03:05:47 +0000 Subject: [PATCH 045/116] Bump jsonschema from 4.18.5 to 4.18.6 Bumps [jsonschema](https://github.com/python-jsonschema/jsonschema) from 4.18.5 to 4.18.6. - [Release notes](https://github.com/python-jsonschema/jsonschema/releases) - [Changelog](https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst) - [Commits](https://github.com/python-jsonschema/jsonschema/compare/v4.18.5...v4.18.6) --- updated-dependencies: - dependency-name: jsonschema 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 e271b7e3a..0e9c0f624 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ arrow==1.2.3 cachetools==5.3.1 requests==2.31.0 urllib3==2.0.4 -jsonschema==4.18.5 +jsonschema==4.18.6 TA-Lib==0.4.27 technical==1.4.0 tabulate==0.9.0 From 4b07720d0bb82a3398df8c3b02caba4ec1f9bd91 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Aug 2023 06:59:16 +0200 Subject: [PATCH 046/116] Update test strategy to ensure we're using stake_amount --- tests/strategy/strats/strategy_test_v3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index 2d5121403..571427fb1 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -197,7 +197,7 @@ class StrategyTestV3(IStrategy): if current_profit < -0.0075: orders = trade.select_filled_orders(trade.entry_side) - return round(orders[0].safe_cost, 0) + return round(orders[0].stake_amount, 0) return None From 03150ee09a9f0ff3cc7d11e8804d8ab9733c76ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Aug 2023 06:59:35 +0200 Subject: [PATCH 047/116] Ensure backpopulated "trade" attribute is immediately loaded. --- freqtrade/persistence/trade_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index f686e4f8c..ab170d307 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -48,7 +48,7 @@ class Order(ModelBase): id: Mapped[int] = mapped_column(Integer, primary_key=True) ft_trade_id: Mapped[int] = mapped_column(Integer, ForeignKey('trades.id'), index=True) - _trade_live: Mapped["Trade"] = relationship("Trade", back_populates="orders") + _trade_live: Mapped["Trade"] = relationship("Trade", back_populates="orders", lazy="immediate") _trade_bt: "LocalTrade" = None # type: ignore # order_side can only be 'buy', 'sell' or 'stoploss' From 9c73e52dd142833cf8d6a4851982b6ccebf3e122 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Aug 2023 06:23:52 +0200 Subject: [PATCH 048/116] Remove sandbox configuration options --- config_examples/config_full.example.json | 1 - docs/configuration.md | 1 - freqtrade/constants.py | 1 - 3 files changed, 3 deletions(-) diff --git a/config_examples/config_full.example.json b/config_examples/config_full.example.json index 42c05eaf5..4681ec7df 100644 --- a/config_examples/config_full.example.json +++ b/config_examples/config_full.example.json @@ -89,7 +89,6 @@ ], "exchange": { "name": "binance", - "sandbox": false, "key": "your_exchange_key", "secret": "your_exchange_secret", "password": "", diff --git a/docs/configuration.md b/docs/configuration.md index e05d1b450..6c0795c67 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -188,7 +188,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `max_entry_position_adjustment` | Maximum additional order(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional orders. [More information here](strategy-callbacks.md#adjust-trade-position).
[Strategy Override](#parameters-in-the-strategy).
*Defaults to `-1`.*
**Datatype:** Positive Integer or -1 | | **Exchange** | `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
**Datatype:** String -| `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.
**Datatype:** Boolean | `exchange.key` | API key to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 4b3cff7a1..fedd34726 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -461,7 +461,6 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'name': {'type': 'string'}, - 'sandbox': {'type': 'boolean', 'default': False}, 'key': {'type': 'string', 'default': ''}, 'secret': {'type': 'string', 'default': ''}, 'password': {'type': 'string', 'default': ''}, From 88d6f70abef64fdd9f4b3275266962d1081a27dd Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Aug 2023 06:25:06 +0200 Subject: [PATCH 049/116] Remove sandbox related code --- freqtrade/exchange/exchange.py | 12 ----------- tests/exchange/test_exchange.py | 35 --------------------------------- 2 files changed, 47 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5f2530431..afbcff3b6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -263,8 +263,6 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e - self.set_sandbox(api, exchange_config, name) - return api @property @@ -465,16 +463,6 @@ class Exchange: return amount_to_contract_precision(amount, self.get_precision_amount(pair), self.precisionMode, contract_size) - def set_sandbox(self, api: ccxt.Exchange, exchange_config: dict, name: str) -> None: - if exchange_config.get('sandbox'): - if api.urls.get('test'): - api.urls['api'] = api.urls['test'] - logger.info("Enabled Sandbox API on %s", name) - else: - logger.warning( - f"No Sandbox URL in CCXT for {name}, exiting. Please check your config.json") - raise OperationalException(f'Exchange {name} does not provide a sandbox api') - def _load_async_markets(self, reload: bool = False) -> None: try: if self._api_async: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 289a67c4a..cba58c825 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -556,41 +556,6 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: assert result == 4000 -def test_set_sandbox(default_conf, mocker): - """ - Test working scenario - """ - api_mock = MagicMock() - api_mock.load_markets = MagicMock(return_value={ - 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' - }) - url_mock = PropertyMock(return_value={'test': "api-public.sandbox.gdax.com", - 'api': 'https://api.gdax.com'}) - type(api_mock).urls = url_mock - exchange = get_patched_exchange(mocker, default_conf, api_mock) - liveurl = exchange._api.urls['api'] - default_conf['exchange']['sandbox'] = True - exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') - assert exchange._api.urls['api'] != liveurl - - -def test_set_sandbox_exception(default_conf, mocker): - """ - Test Fail scenario - """ - api_mock = MagicMock() - api_mock.load_markets = MagicMock(return_value={ - 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' - }) - url_mock = PropertyMock(return_value={'api': 'https://api.gdax.com'}) - type(api_mock).urls = url_mock - - with pytest.raises(OperationalException, match=r'does not provide a sandbox api'): - exchange = get_patched_exchange(mocker, default_conf, api_mock) - default_conf['exchange']['sandbox'] = True - exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') - - def test__load_async_markets(default_conf, mocker, caplog): mocker.patch(f'{EXMS}._init_ccxt') mocker.patch(f'{EXMS}.validate_pairs') From 5e3e443d27245e52a256c2b8329724a6fe2dc8a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Aug 2023 06:25:12 +0200 Subject: [PATCH 050/116] Remove Sandbox docs --- docs/sandbox-testing.md | 121 ---------------------------------------- mkdocs.yml | 1 - 2 files changed, 122 deletions(-) delete mode 100644 docs/sandbox-testing.md diff --git a/docs/sandbox-testing.md b/docs/sandbox-testing.md deleted file mode 100644 index 2c0f306cf..000000000 --- a/docs/sandbox-testing.md +++ /dev/null @@ -1,121 +0,0 @@ -# Sandbox API testing - -Some exchanges provide sandboxes or testbeds for risk-free testing, while running the bot against a real exchange. -With some configuration, freqtrade (in combination with ccxt) provides access to these. - -This document is an overview to configure Freqtrade to be used with sandboxes. -This can be useful to developers and trader alike. - -!!! Warning - Sandboxes usually have very low volume, and either a very wide spread, or no orders available at all. - Therefore, sandboxes will usually not do a good job of showing you how a strategy would work in real trading. - -## Exchanges known to have a sandbox / testnet - -* [binance](https://testnet.binance.vision/) -* [coinbasepro](https://public.sandbox.pro.coinbase.com) -* [gemini](https://exchange.sandbox.gemini.com/) -* [huobipro](https://www.testnet.huobi.pro/) -* [kucoin](https://sandbox.kucoin.com/) -* [phemex](https://testnet.phemex.com/) - -!!! Note - We did not test correct functioning of all of the above testnets. Please report your experiences with each sandbox. - ---- - -## Configure a Sandbox account - -When testing your API connectivity, make sure to use the appropriate sandbox / testnet URL. - -In general, you should follow these steps to enable an exchange's sandbox: - -* Figure out if an exchange has a sandbox (most likely by using google or the exchange's support documents) -* Create a sandbox account (often the sandbox-account requires separate registration) -* [Add some test assets to account](#add-test-funds) -* Create API keys - -### Add test funds - -Usually, sandbox exchanges allow depositing funds directly via web-interface. -You should make sure to have a realistic amount of funds available to your test-account, so results are representable of your real account funds. - -!!! Warning - Test exchanges will **NEVER** require your real credit card or banking details! - -## Configure freqtrade to use a exchange's sandbox - -### Sandbox URLs - -Freqtrade makes use of CCXT which in turn provides a list of URLs to Freqtrade. -These include `['test']` and `['api']`. - -* `[Test]` if available will point to an Exchanges sandbox. -* `[Api]` normally used, and resolves to live API target on the exchange. - -To make use of sandbox / test add "sandbox": true, to your config.json - -```json - "exchange": { - "name": "coinbasepro", - "sandbox": true, - "key": "5wowfxemogxeowo;heiohgmd", - "secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==", - "password": "1bkjfkhfhfu6sr", - "outdated_offset": 5 - "pair_whitelist": [ - "BTC/USD" - ] - }, - "datadir": "user_data/data/coinbasepro_sandbox" -``` - -Also the following information: - -* api-key (created for the sandbox webpage) -* api-secret (noted earlier) -* password (the passphrase - noted earlier) - -!!! Tip "Different data directory" - We also recommend to set `datadir` to something identifying downloaded data as sandbox data, to avoid having sandbox data mixed with data from the real exchange. - This can be done by adding the `"datadir"` key to the configuration. - Now, whenever you use this configuration, your data directory will be set to this directory. - ---- - -## You should now be ready to test your sandbox - -Ensure Freqtrade logs show the sandbox URL, and trades made are shown in sandbox. Also make sure to select a pair which shows at least some decent value (which very often is BTC/). - -## Common problems with sandbox exchanges - -Sandbox exchange instances often have very low volume, which can cause some problems which usually are not seen on a real exchange instance. - -### Old Candles problem - -Since Sandboxes often have low volume, candles can be quite old and show no volume. -To disable the error "Outdated history for pair ...", best increase the parameter `"outdated_offset"` to a number that seems realistic for the sandbox you're using. - -### Unfilled orders - -Sandboxes often have very low volumes - which means that many trades can go unfilled, or can go unfilled for a very long time. - -To mitigate this, you can try to match the first order on the opposite orderbook side using the following configuration: - -``` jsonc - "order_types": { - "entry": "limit", - "exit": "limit" - // ... - }, - "entry_pricing": { - "price_side": "other", - // ... - }, - "exit_pricing":{ - "price_side": "other", - // ... - }, - ``` - - The configuration is similar to the suggested configuration for market orders - however by using limit-orders you can avoid moving the price too much, and you can set the worst price you might get. diff --git a/mkdocs.yml b/mkdocs.yml index 815a10419..bb5ae0010 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -47,7 +47,6 @@ nav: - Advanced Hyperopt: advanced-hyperopt.md - Producer/Consumer mode: producer-consumer.md - Edge Positioning: edge.md - - Sandbox Testing: sandbox-testing.md - FAQ: faq.md - SQL Cheat-sheet: sql_cheatsheet.md - Strategy migration: strategy_migration.md From 05bbc8e7aaea58c9758f5c577ec52b7e33580fb5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Aug 2023 06:26:25 +0200 Subject: [PATCH 051/116] Remove last sandbox occurance --- tests/test_configuration.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 7808fb5c8..3f2fb0669 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1038,8 +1038,7 @@ def test_load_config_stoploss_exchange_limit_ratio(all_conf) -> None: validate_config_schema(all_conf) -@pytest.mark.parametrize("keys", [("exchange", "sandbox", False), - ("exchange", "key", ""), +@pytest.mark.parametrize("keys", [("exchange", "key", ""), ("exchange", "secret", ""), ("exchange", "password", ""), ]) From c88f71c63846aa8b81cf75dd361ad68309a901af Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Tue, 8 Aug 2023 14:57:48 +0900 Subject: [PATCH 052/116] add timeout to discord --- freqtrade/rpc/discord.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/rpc/discord.py b/freqtrade/rpc/discord.py index 8be0eab68..36ef37d01 100644 --- a/freqtrade/rpc/discord.py +++ b/freqtrade/rpc/discord.py @@ -20,6 +20,7 @@ class Discord(Webhook): self._format = 'json' self._retries = 1 self._retry_delay = 0.1 + self._timeout = self._config['discord'].get('timeout', 10) def cleanup(self) -> None: """ From 1f23727ff79052bc59bd29d756250a5ae93bef59 Mon Sep 17 00:00:00 2001 From: Jan Smets Date: Tue, 8 Aug 2023 11:36:48 +0200 Subject: [PATCH 053/116] Increase bybit ohlcv_candle_limit to 1000 --- freqtrade/exchange/bybit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 145a501c9..84779ea72 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -27,7 +27,7 @@ class Bybit(Exchange): """ _ft_has: Dict = { - "ohlcv_candle_limit": 200, + "ohlcv_candle_limit": 1000, "ohlcv_has_history": True, } _ft_has_futures: Dict = { From ab156b6ad7b216486a795008f977d934cb3621d2 Mon Sep 17 00:00:00 2001 From: Jan Smets Date: Tue, 8 Aug 2023 12:28:28 +0200 Subject: [PATCH 054/116] Increase bybit ohlcv_candle_limit to 1000 in tests --- tests/exchange/test_bybit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_bybit.py b/tests/exchange/test_bybit.py index d0d5114a1..a8c902fd6 100644 --- a/tests/exchange/test_bybit.py +++ b/tests/exchange/test_bybit.py @@ -26,7 +26,7 @@ async def test_bybit_fetch_funding_rate(default_conf, mocker): api_mock = MagicMock() api_mock.fetch_funding_rate_history = get_mock_coro(return_value=[]) exchange = get_patched_exchange(mocker, default_conf, id='bybit', api_mock=api_mock) - limit = 200 + limit = 1000 # Test fetch_funding_rate_history (current data) await exchange._fetch_funding_rate_history( pair='BTC/USDT:USDT', From 62ad2cca1abf0d0f31682d6450baae10593a83e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Aug 2023 20:14:37 +0200 Subject: [PATCH 055/116] Add active test for alternative futures rates (ensures history is loaded correctly). --- tests/exchange/test_ccxt_compat.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 0f8c43876..2c370145d 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -511,7 +511,8 @@ class TestCCXTExchange: now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2)) assert exch.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now) - def ccxt__async_get_candle_history(self, exchange, exchangename, pair, timeframe, candle_type): + def ccxt__async_get_candle_history( + self, exchange, exchangename, pair, timeframe, candle_type, factor=0.9): timeframe_ms = timeframe_to_msecs(timeframe) now = timeframe_to_prev_date( @@ -532,11 +533,11 @@ class TestCCXTExchange: assert res[1] == timeframe assert res[2] == candle_type candles = res[3] - factor = 0.9 candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * factor candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms * factor assert len(candles) >= min(candle_count, candle_count1), \ f"{len(candles)} < {candle_count} in {timeframe}, Offset: {offset} {factor}" + # Check if first-timeframe is either the start, or start + 1 assert candles[0][0] == since_ms or (since_ms + timeframe_ms) def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE): @@ -552,15 +553,29 @@ class TestCCXTExchange: self.ccxt__async_get_candle_history( exc, exchangename, pair, timeframe, CandleType.SPOT) - def test_ccxt__async_get_candle_history_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): + @pytest.mark.parametrize('candle_type', [ + CandleType.FUTURES, + CandleType.FUNDING_RATE, + CandleType.MARK, + ]) + def test_ccxt__async_get_candle_history_futures( + self, exchange_futures: EXCHANGE_FIXTURE_TYPE, candle_type): exchange, exchangename = exchange_futures if not exchange: # exchange_futures only returns values for supported exchanges return pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair']) timeframe = EXCHANGES[exchangename]['timeframe'] + if candle_type == CandleType.FUNDING_RATE: + timeframe = exchange._ft_has.get('funding_fee_timeframe', + exchange._ft_has['mark_ohlcv_timeframe']) self.ccxt__async_get_candle_history( - exchange, exchangename, pair, timeframe, CandleType.FUTURES) + exchange, + exchangename, + pair=pair, + timeframe=timeframe, + candle_type=candle_type, + ) def test_ccxt_fetch_funding_rate_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): exchange, exchangename = exchange_futures From 565e2699b4618a81aeefaee885bf343b0d4491c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Aug 2023 20:23:01 +0200 Subject: [PATCH 056/116] Re-set funding-fee history limit for bybit to 200 --- freqtrade/exchange/bybit.py | 12 +++++++++++- tests/exchange/test_bybit.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 84779ea72..5f3762453 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -7,6 +7,7 @@ import ccxt from freqtrade.constants import BuySell from freqtrade.enums import MarginMode, PriceType, TradingMode +from freqtrade.enums.candletype import CandleType from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier @@ -91,6 +92,14 @@ class Bybit(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e + def ohlcv_candle_limit( + self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int: + + if candle_type in (CandleType.FUNDING_RATE): + return 200 + + return super().ohlcv_candle_limit(timeframe, candle_type, since_ms) + async def _fetch_funding_rate_history( self, pair: str, @@ -104,7 +113,8 @@ class Bybit(Exchange): """ params = {} if since_ms: - until = since_ms + (timeframe_to_msecs(timeframe) * self._ft_has['ohlcv_candle_limit']) + limit = self.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, since_ms) + until = since_ms + (timeframe_to_msecs(timeframe) * limit) params.update({'until': until}) # Funding rate data = await self._api_async.fetch_funding_rate_history( diff --git a/tests/exchange/test_bybit.py b/tests/exchange/test_bybit.py index a8c902fd6..d0d5114a1 100644 --- a/tests/exchange/test_bybit.py +++ b/tests/exchange/test_bybit.py @@ -26,7 +26,7 @@ async def test_bybit_fetch_funding_rate(default_conf, mocker): api_mock = MagicMock() api_mock.fetch_funding_rate_history = get_mock_coro(return_value=[]) exchange = get_patched_exchange(mocker, default_conf, id='bybit', api_mock=api_mock) - limit = 1000 + limit = 200 # Test fetch_funding_rate_history (current data) await exchange._fetch_funding_rate_history( pair='BTC/USDT:USDT', From 78cf8a1c095123a82313fb14cfb4b2e74fe2e739 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Aug 2023 20:31:10 +0200 Subject: [PATCH 057/116] Fix exchange bybit test --- tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 289a67c4a..a8c765553 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2100,7 +2100,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_ exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) # one_call calculation * 1.8 should do 2 calls - since = 5 * 60 * exchange.ohlcv_candle_limit('5m', CandleType.SPOT) * 1.8 + since = 5 * 60 * exchange.ohlcv_candle_limit('5m', candle_type) * 1.8 ret = exchange.get_historic_ohlcv( pair, "5m", From 2069abe31411fa67ba330dd8b650d19eb03380a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Aug 2023 20:54:58 +0200 Subject: [PATCH 058/116] Remove custom fetch_funding_fees from bybit --- freqtrade/exchange/bybit.py | 25 ------------------------- tests/exchange/test_bybit.py | 4 ---- 2 files changed, 29 deletions(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 5f3762453..626643d06 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -11,7 +11,6 @@ from freqtrade.enums.candletype import CandleType from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier -from freqtrade.exchange.exchange_utils import timeframe_to_msecs logger = logging.getLogger(__name__) @@ -100,30 +99,6 @@ class Bybit(Exchange): return super().ohlcv_candle_limit(timeframe, candle_type, since_ms) - async def _fetch_funding_rate_history( - self, - pair: str, - timeframe: str, - limit: int, - since_ms: Optional[int] = None, - ) -> List[List]: - """ - Fetch funding rate history - Necessary workaround until https://github.com/ccxt/ccxt/issues/15990 is fixed. - """ - params = {} - if since_ms: - limit = self.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, since_ms) - until = since_ms + (timeframe_to_msecs(timeframe) * limit) - params.update({'until': until}) - # Funding rate - data = await self._api_async.fetch_funding_rate_history( - pair, since=since_ms, - params=params) - # Convert funding rate to candle pattern - data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data] - return data - def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False): if self.trading_mode != TradingMode.SPOT: params = {'leverage': leverage} diff --git a/tests/exchange/test_bybit.py b/tests/exchange/test_bybit.py index d0d5114a1..7495f543b 100644 --- a/tests/exchange/test_bybit.py +++ b/tests/exchange/test_bybit.py @@ -3,7 +3,6 @@ from unittest.mock import MagicMock from freqtrade.enums.marginmode import MarginMode from freqtrade.enums.tradingmode import TradingMode -from freqtrade.exchange.exchange_utils import timeframe_to_msecs from tests.conftest import get_mock_coro, get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -37,12 +36,10 @@ async def test_bybit_fetch_funding_rate(default_conf, mocker): assert api_mock.fetch_funding_rate_history.call_count == 1 assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT' kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1] - assert kwargs['params'] == {} assert kwargs['since'] is None api_mock.fetch_funding_rate_history.reset_mock() since_ms = 1610000000000 - since_ms_end = since_ms + (timeframe_to_msecs('4h') * limit) # Test fetch_funding_rate_history (current data) await exchange._fetch_funding_rate_history( pair='BTC/USDT:USDT', @@ -54,7 +51,6 @@ async def test_bybit_fetch_funding_rate(default_conf, mocker): assert api_mock.fetch_funding_rate_history.call_count == 1 assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT' kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1] - assert kwargs['params'] == {'until': since_ms_end} assert kwargs['since'] == since_ms From 4a62ebbf9369904200499c7a23bb5732f6586900 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Aug 2023 18:36:09 +0200 Subject: [PATCH 059/116] Don't hardcode fee, but use fee from the very first iteration. --- freqtrade/optimize/lookahead_analysis.py | 8 +++++++- freqtrade/optimize/lookahead_analysis_helpers.py | 4 ---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/lookahead_analysis.py b/freqtrade/optimize/lookahead_analysis.py index f363ae196..0543fcde7 100755 --- a/freqtrade/optimize/lookahead_analysis.py +++ b/freqtrade/optimize/lookahead_analysis.py @@ -48,6 +48,7 @@ class LookaheadAnalysis: self.entry_varHolders: List[VarHolder] = [] self.exit_varHolders: List[VarHolder] = [] self.exchange: Optional[Any] = None + self._fee = None # pull variables the scope of the lookahead_analysis-instance self.local_config = deepcopy(config) @@ -145,8 +146,13 @@ class LookaheadAnalysis: str(self.dt_to_timestamp(varholder.to_dt))) prepare_data_config['exchange']['pair_whitelist'] = pairs_to_load + if self._fee: + # Don't re-calculate fee per pair, as fee might differ per pair. + prepare_data_config['fee'] = self._fee + backtesting = Backtesting(prepare_data_config, self.exchange) self.exchange = backtesting.exchange + self._fee = backtesting.fee backtesting._set_strategy(backtesting.strategylist[0]) varholder.data, varholder.timerange = backtesting.load_bt_data() @@ -198,7 +204,7 @@ class LookaheadAnalysis: self.prepare_data(exit_varHolder, [result_row['pair']]) # now we analyze a full trade of full_varholder and look for analyze its bias - def analyze_row(self, idx, result_row): + def analyze_row(self, idx: int, result_row): # if force-sold, ignore this signal since here it will unconditionally exit. if result_row.close_date == self.dt_to_timestamp(self.full_varHolder.to_dt): return diff --git a/freqtrade/optimize/lookahead_analysis_helpers.py b/freqtrade/optimize/lookahead_analysis_helpers.py index 654ff93d2..422026780 100644 --- a/freqtrade/optimize/lookahead_analysis_helpers.py +++ b/freqtrade/optimize/lookahead_analysis_helpers.py @@ -137,10 +137,6 @@ class LookaheadAnalysisSubFunctions: 'just to avoid false positives') config['dry_run_wallet'] = min_dry_run_wallet - if 'fee' not in config or config['fee'] != 0.02: - logger.info('fee was not set to a fixed value of 0.02. ') - config['fee'] = 0.02 - if 'timerange' not in config: # setting a timerange is enforced here raise OperationalException( From b93464403997d5e272a07b66474d2d1eb4526b45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Aug 2023 18:36:20 +0200 Subject: [PATCH 060/116] Fix tests, explicitly test for missing timerange --- tests/optimize/test_lookahead_analysis.py | 24 ++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_lookahead_analysis.py b/tests/optimize/test_lookahead_analysis.py index 3c6a5ad6d..decc4706d 100644 --- a/tests/optimize/test_lookahead_analysis.py +++ b/tests/optimize/test_lookahead_analysis.py @@ -17,6 +17,8 @@ from tests.conftest import EXMS, get_args, log_has_re, patch_exchange def lookahead_conf(default_conf_usdt): default_conf_usdt['minimum_trade_amount'] = 10 default_conf_usdt['targeted_trade_amount'] = 20 + default_conf_usdt['timerange'] = '20220101-20220501' + default_conf_usdt['strategy_path'] = str( Path(__file__).parent.parent / "strategy/strats/lookahead_bias") default_conf_usdt['strategy'] = 'strategy_test_v3_with_lookahead_bias' @@ -43,7 +45,9 @@ def test_start_lookahead_analysis(mocker): "--pairs", "UNITTEST/BTC", "--max-open-trades", - "1" + "1", + "--timerange", + "20220101-20220201" ] pargs = get_args(args) pargs['config'] = None @@ -72,6 +76,24 @@ def test_start_lookahead_analysis(mocker): match=r"Targeted trade amount can't be smaller than minimum trade amount.*"): start_lookahead_analysis(pargs) + # Missing timerange + args = [ + "lookahead-analysis", + "--strategy", + "strategy_test_v3_with_lookahead_bias", + "--strategy-path", + str(Path(__file__).parent.parent / "strategy/strats/lookahead_bias"), + "--pairs", + "UNITTEST/BTC", + "--max-open-trades", + "1", + ] + pargs = get_args(args) + pargs['config'] = None + with pytest.raises(OperationalException, + match=r"Please set a timerange\..*"): + start_lookahead_analysis(pargs) + def test_lookahead_helper_invalid_config(lookahead_conf) -> None: conf = deepcopy(lookahead_conf) From 328a6f791e4057f30011a20e96019b61895c3a46 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Aug 2023 19:55:27 +0200 Subject: [PATCH 061/116] Improve stoploss mock --- tests/test_freqtradebot.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index e533acbb8..f595788dd 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1508,15 +1508,15 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, @pytest.mark.parametrize("is_short", [False, True]) def test_create_stoploss_order_invalid_order( - mocker, default_conf_usdt, caplog, fee, is_short, limit_order, limit_order_open + mocker, default_conf_usdt, caplog, fee, is_short, limit_order ): - open_order = limit_order_open[entry_side(is_short)] + open_order = limit_order[entry_side(is_short)] order = limit_order[exit_side(is_short)] rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) create_order_mock = MagicMock(side_effect=[ open_order, - {'id': order['id']} + order, ]) mocker.patch.multiple( EXMS, @@ -1541,6 +1541,7 @@ def test_create_stoploss_order_invalid_order( trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short caplog.clear() + rpc_mock.reset_mock() freqtrade.create_stoploss_order(trade, 200) assert trade.stoploss_order_id is None assert trade.exit_reason == ExitType.EMERGENCY_EXIT.value @@ -1554,9 +1555,11 @@ def test_create_stoploss_order_invalid_order( assert create_order_mock.call_args[1]['amount'] == trade.amount # Rpc is sending first buy, then sell - assert rpc_mock.call_count == 3 - assert rpc_mock.call_args_list[2][0][0]['sell_reason'] == ExitType.EMERGENCY_EXIT.value - assert rpc_mock.call_args_list[2][0][0]['order_type'] == 'market' + assert rpc_mock.call_count == 2 + assert rpc_mock.call_args_list[0][0][0]['sell_reason'] == ExitType.EMERGENCY_EXIT.value + assert rpc_mock.call_args_list[0][0][0]['order_type'] == 'market' + assert rpc_mock.call_args_list[0][0][0]['type'] == 'exit' + assert rpc_mock.call_args_list[1][0][0]['type'] == 'exit_fill' @pytest.mark.parametrize("is_short", [False, True]) From 05e1828617d27d71c1d2d84967aca2b2f92d3404 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Aug 2023 20:26:08 +0200 Subject: [PATCH 062/116] Improve Fee check --- freqtrade/optimize/lookahead_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/lookahead_analysis.py b/freqtrade/optimize/lookahead_analysis.py index 0543fcde7..80418da95 100755 --- a/freqtrade/optimize/lookahead_analysis.py +++ b/freqtrade/optimize/lookahead_analysis.py @@ -146,7 +146,7 @@ class LookaheadAnalysis: str(self.dt_to_timestamp(varholder.to_dt))) prepare_data_config['exchange']['pair_whitelist'] = pairs_to_load - if self._fee: + if self._fee is not None: # Don't re-calculate fee per pair, as fee might differ per pair. prepare_data_config['fee'] = self._fee From 7e9389421a80b70e477caa0210c022e9a8eaa54c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Aug 2023 07:02:55 +0200 Subject: [PATCH 063/116] Move ccxt_compat tests to their own subfolder --- tests/exchange_online/__init__.py | 0 tests/{exchange => exchange_online}/test_ccxt_compat.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/exchange_online/__init__.py rename tests/{exchange => exchange_online}/test_ccxt_compat.py (100%) diff --git a/tests/exchange_online/__init__.py b/tests/exchange_online/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange_online/test_ccxt_compat.py similarity index 100% rename from tests/exchange/test_ccxt_compat.py rename to tests/exchange_online/test_ccxt_compat.py From 20763daa7437d12c157310d2dfdbd5e23f78899a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Aug 2023 07:10:45 +0200 Subject: [PATCH 064/116] Simplify online tess by skipping non-available futures exchanges --- tests/exchange_online/test_ccxt_compat.py | 68 +++++++++-------------- 1 file changed, 25 insertions(+), 43 deletions(-) diff --git a/tests/exchange_online/test_ccxt_compat.py b/tests/exchange_online/test_ccxt_compat.py index 2c370145d..0aa59db2a 100644 --- a/tests/exchange_online/test_ccxt_compat.py +++ b/tests/exchange_online/test_ccxt_compat.py @@ -314,7 +314,7 @@ def exchange(request, exchange_conf): @pytest.fixture(params=EXCHANGES, scope="class") def exchange_futures(request, exchange_conf, class_mocker): if EXCHANGES[request.param].get('futures') is not True: - yield None, request.param + pytest.skip(f"Exchange {request.param} does not support futures.") else: exchange_conf = set_test_proxy( exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False)) @@ -371,9 +371,6 @@ class TestCCXTExchange: def test_load_markets_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): exchange, exchangename = exchange_futures - if not exchange: - # exchange_futures only returns values for supported exchanges - return pair = EXCHANGES[exchangename]['pair'] pair = EXCHANGES[exchangename].get('futures_pair', pair) markets = exchange.markets @@ -561,9 +558,6 @@ class TestCCXTExchange: def test_ccxt__async_get_candle_history_futures( self, exchange_futures: EXCHANGE_FIXTURE_TYPE, candle_type): exchange, exchangename = exchange_futures - if not exchange: - # exchange_futures only returns values for supported exchanges - return pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair']) timeframe = EXCHANGES[exchangename]['timeframe'] if candle_type == CandleType.FUNDING_RATE: @@ -579,9 +573,6 @@ class TestCCXTExchange: def test_ccxt_fetch_funding_rate_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): exchange, exchangename = exchange_futures - if not exchange: - # exchange_futures only returns values for supported exchanges - return pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair']) since = int((datetime.now(timezone.utc) - timedelta(days=5)).timestamp() * 1000) @@ -617,9 +608,6 @@ class TestCCXTExchange: def test_ccxt_fetch_mark_price_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): exchange, exchangename = exchange_futures - if not exchange: - # exchange_futures only returns values for supported exchanges - return pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair']) since = int((datetime.now(timezone.utc) - timedelta(days=5)).timestamp() * 1000) pair_tf = (pair, '1h', CandleType.MARK) @@ -641,9 +629,6 @@ class TestCCXTExchange: def test_ccxt__calculate_funding_fees(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): exchange, exchangename = exchange_futures - if not exchange: - # exchange_futures only returns values for supported exchanges - return pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair']) since = datetime.now(timezone.utc) - timedelta(days=5) @@ -690,31 +675,29 @@ class TestCCXTExchange: def test_ccxt_get_max_leverage_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): futures, futures_name = exchange_futures - if futures: - leverage_tiers_public = EXCHANGES[futures_name].get('leverage_tiers_public') - if leverage_tiers_public: - futures_pair = EXCHANGES[futures_name].get( - 'futures_pair', - EXCHANGES[futures_name]['pair'] - ) - futures_leverage = futures.get_max_leverage(futures_pair, 20) - assert (isinstance(futures_leverage, float) or isinstance(futures_leverage, int)) - assert futures_leverage >= 1.0 - - def test_ccxt_get_contract_size(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): - futures, futures_name = exchange_futures - if futures: + leverage_tiers_public = EXCHANGES[futures_name].get('leverage_tiers_public') + if leverage_tiers_public: futures_pair = EXCHANGES[futures_name].get( 'futures_pair', EXCHANGES[futures_name]['pair'] ) - contract_size = futures.get_contract_size(futures_pair) - assert (isinstance(contract_size, float) or isinstance(contract_size, int)) - assert contract_size >= 0.0 + futures_leverage = futures.get_max_leverage(futures_pair, 20) + assert (isinstance(futures_leverage, float) or isinstance(futures_leverage, int)) + assert futures_leverage >= 1.0 + + def test_ccxt_get_contract_size(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): + futures, futures_name = exchange_futures + futures_pair = EXCHANGES[futures_name].get( + 'futures_pair', + EXCHANGES[futures_name]['pair'] + ) + contract_size = futures.get_contract_size(futures_pair) + assert (isinstance(contract_size, float) or isinstance(contract_size, int)) + assert contract_size >= 0.0 def test_ccxt_load_leverage_tiers(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): futures, futures_name = exchange_futures - if futures and EXCHANGES[futures_name].get('leverage_tiers_public'): + if EXCHANGES[futures_name].get('leverage_tiers_public'): leverage_tiers = futures.load_leverage_tiers() futures_pair = EXCHANGES[futures_name].get( 'futures_pair', @@ -747,7 +730,7 @@ class TestCCXTExchange: def test_ccxt_dry_run_liquidation_price(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): futures, futures_name = exchange_futures - if futures and EXCHANGES[futures_name].get('leverage_tiers_public'): + if EXCHANGES[futures_name].get('leverage_tiers_public'): futures_pair = EXCHANGES[futures_name].get( 'futures_pair', @@ -780,14 +763,13 @@ class TestCCXTExchange: def test_ccxt_get_max_pair_stake_amount(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): futures, futures_name = exchange_futures - if futures: - futures_pair = EXCHANGES[futures_name].get( - 'futures_pair', - EXCHANGES[futures_name]['pair'] - ) - max_stake_amount = futures.get_max_pair_stake_amount(futures_pair, 40000) - assert (isinstance(max_stake_amount, float)) - assert max_stake_amount >= 0.0 + futures_pair = EXCHANGES[futures_name].get( + 'futures_pair', + EXCHANGES[futures_name]['pair'] + ) + max_stake_amount = futures.get_max_pair_stake_amount(futures_pair, 40000) + assert (isinstance(max_stake_amount, float)) + assert max_stake_amount >= 0.0 def test_private_method_presence(self, exchange: EXCHANGE_FIXTURE_TYPE): exch, exchangename = exchange From ea257e3cbb4bf39f2205739947dfa3e69d749a1b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Aug 2023 07:17:31 +0200 Subject: [PATCH 065/116] Refactor online test fixtures into separate conftest module --- tests/exchange_online/conftest.py | 329 ++++++++++++++++++++++ tests/exchange_online/test_ccxt_compat.py | 328 +-------------------- 2 files changed, 331 insertions(+), 326 deletions(-) create mode 100644 tests/exchange_online/conftest.py diff --git a/tests/exchange_online/conftest.py b/tests/exchange_online/conftest.py new file mode 100644 index 000000000..f4b50778d --- /dev/null +++ b/tests/exchange_online/conftest.py @@ -0,0 +1,329 @@ +from copy import deepcopy +from pathlib import Path +from typing import Tuple + +import pytest + +from freqtrade.constants import Config +from freqtrade.exchange.exchange import Exchange +from freqtrade.resolvers.exchange_resolver import ExchangeResolver +from tests.conftest import EXMS, get_default_conf_usdt + + +EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str] + +# Exchanges that should be tested online +EXCHANGES = { + 'bittrex': { + 'pair': 'BTC/USDT', + 'stake_currency': 'USDT', + 'hasQuoteVolume': False, + 'timeframe': '1h', + 'leverage_tiers_public': False, + 'leverage_in_spot_market': False, + }, + 'binance': { + 'pair': 'BTC/USDT', + 'stake_currency': 'USDT', + 'use_ci_proxy': True, + 'hasQuoteVolume': True, + 'timeframe': '1h', + 'futures': True, + 'futures_pair': 'BTC/USDT:USDT', + 'hasQuoteVolumeFutures': True, + 'leverage_tiers_public': False, + 'leverage_in_spot_market': False, + 'trades_lookback_hours': 4, + 'private_methods': [ + 'fapiPrivateGetPositionSideDual', + 'fapiPrivateGetMultiAssetsMargin' + ], + 'sample_order': [{ + "symbol": "SOLUSDT", + "orderId": 3551312894, + "orderListId": -1, + "clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba", + "transactTime": 1674493798550, + "price": "15.50000000", + "origQty": "1.10000000", + "executedQty": "0.00000000", + "cummulativeQuoteQty": "0.00000000", + "status": "NEW", + "timeInForce": "GTC", + "type": "LIMIT", + "side": "BUY", + "workingTime": 1674493798550, + "fills": [], + "selfTradePreventionMode": "NONE", + }] + }, + 'binanceus': { + 'pair': 'BTC/USDT', + 'stake_currency': 'USDT', + 'hasQuoteVolume': True, + 'timeframe': '1h', + 'futures': False, + 'sample_order': [{ + "symbol": "SOLUSDT", + "orderId": 3551312894, + "orderListId": -1, + "clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba", + "transactTime": 1674493798550, + "price": "15.50000000", + "origQty": "1.10000000", + "executedQty": "0.00000000", + "cummulativeQuoteQty": "0.00000000", + "status": "NEW", + "timeInForce": "GTC", + "type": "LIMIT", + "side": "BUY", + "workingTime": 1674493798550, + "fills": [], + "selfTradePreventionMode": "NONE", + }] + }, + 'kraken': { + 'pair': 'BTC/USD', + 'stake_currency': 'USD', + 'hasQuoteVolume': True, + 'timeframe': '1h', + 'leverage_tiers_public': False, + 'leverage_in_spot_market': True, + 'trades_lookback_hours': 12, + }, + 'kucoin': { + 'pair': 'XRP/USDT', + 'stake_currency': 'USDT', + 'hasQuoteVolume': True, + 'timeframe': '1h', + 'leverage_tiers_public': False, + 'leverage_in_spot_market': True, + 'sample_order': [ + {'id': '63d6742d0adc5570001d2bbf7'}, # create order + { + 'id': '63d6742d0adc5570001d2bbf7', + 'symbol': 'SOL-USDT', + 'opType': 'DEAL', + 'type': 'limit', + 'side': 'buy', + 'price': '15.5', + 'size': '1.1', + 'funds': '0', + 'dealFunds': '17.05', + 'dealSize': '1.1', + 'fee': '0.000065252', + 'feeCurrency': 'USDT', + 'stp': '', + 'stop': '', + 'stopTriggered': False, + 'stopPrice': '0', + 'timeInForce': 'GTC', + 'postOnly': False, + 'hidden': False, + 'iceberg': False, + 'visibleSize': '0', + 'cancelAfter': 0, + 'channel': 'API', + 'clientOid': '0a053870-11bf-41e5-be61-b272a4cb62e1', + 'remark': None, + 'tags': 'partner:ccxt', + 'isActive': False, + 'cancelExist': False, + 'createdAt': 1674493798550, + 'tradeType': 'TRADE' + }], + }, + 'gate': { + 'pair': 'BTC/USDT', + 'stake_currency': 'USDT', + 'hasQuoteVolume': True, + 'timeframe': '1h', + 'futures': True, + 'futures_pair': 'BTC/USDT:USDT', + 'hasQuoteVolumeFutures': True, + 'leverage_tiers_public': True, + 'leverage_in_spot_market': True, + 'sample_order': [ + { + "id": "276266139423", + "text": "apiv4", + "create_time": "1674493798", + "update_time": "1674493798", + "create_time_ms": "1674493798550", + "update_time_ms": "1674493798550", + "status": "closed", + "currency_pair": "SOL_USDT", + "type": "limit", + "account": "spot", + "side": "buy", + "amount": "1.1", + "price": "15.5", + "time_in_force": "gtc", + "iceberg": "0", + "left": "0", + "fill_price": "17.05", + "filled_total": "17.05", + "avg_deal_price": "15.5", + "fee": "0.0000018", + "fee_currency": "SOL", + "point_fee": "0", + "gt_fee": "0", + "gt_maker_fee": "0", + "gt_taker_fee": "0.0015", + "gt_discount": True, + "rebated_fee": "0", + "rebated_fee_currency": "USDT" + }, + { + # market order + 'id': '276401180529', + 'text': 'apiv4', + 'create_time': '1674493798', + 'update_time': '1674493798', + 'create_time_ms': '1674493798550', + 'update_time_ms': '1674493798550', + 'status': 'cancelled', + 'currency_pair': 'SOL_USDT', + 'type': 'market', + 'account': 'spot', + 'side': 'buy', + 'amount': '17.05', + 'price': '0', + 'time_in_force': 'ioc', + 'iceberg': '0', + 'left': '0.0000000016228', + 'fill_price': '17.05', + 'filled_total': '17.05', + 'avg_deal_price': '15.5', + 'fee': '0', + 'fee_currency': 'SOL', + 'point_fee': '0.0199999999967544', + 'gt_fee': '0', + 'gt_maker_fee': '0', + 'gt_taker_fee': '0', + 'gt_discount': False, + 'rebated_fee': '0', + 'rebated_fee_currency': 'USDT' + } + ], + }, + 'okx': { + 'pair': 'BTC/USDT', + 'stake_currency': 'USDT', + 'hasQuoteVolume': True, + 'timeframe': '1h', + 'futures': True, + 'futures_pair': 'BTC/USDT:USDT', + 'hasQuoteVolumeFutures': False, + 'leverage_tiers_public': True, + 'leverage_in_spot_market': True, + 'private_methods': ['fetch_accounts'], + }, + 'bybit': { + 'pair': 'BTC/USDT', + 'stake_currency': 'USDT', + 'hasQuoteVolume': True, + 'use_ci_proxy': True, + 'timeframe': '1h', + 'futures_pair': 'BTC/USDT:USDT', + 'futures': True, + 'leverage_tiers_public': True, + 'leverage_in_spot_market': True, + 'sample_order': [ + { + "orderId": "1274754916287346280", + "orderLinkId": "1666798627015730", + "symbol": "SOLUSDT", + "createTime": "1674493798550", + "orderPrice": "15.5", + "orderQty": "1.1", + "orderType": "LIMIT", + "side": "BUY", + "status": "NEW", + "timeInForce": "GTC", + "accountId": "5555555", + "execQty": "0", + "orderCategory": "0" + } + ] + }, + 'huobi': { + 'pair': 'ETH/BTC', + 'stake_currency': 'BTC', + 'hasQuoteVolume': True, + 'timeframe': '1h', + 'futures': False, + }, + 'bitvavo': { + 'pair': 'BTC/EUR', + 'stake_currency': 'EUR', + 'hasQuoteVolume': True, + 'timeframe': '1h', + 'leverage_tiers_public': False, + 'leverage_in_spot_market': False, + }, +} + + +@pytest.fixture(scope="class") +def exchange_conf(): + config = get_default_conf_usdt((Path(__file__).parent / "testdata").resolve()) + config['exchange']['pair_whitelist'] = [] + config['exchange']['key'] = '' + config['exchange']['secret'] = '' + config['dry_run'] = False + config['entry_pricing']['use_order_book'] = True + config['exit_pricing']['use_order_book'] = True + return config + + +def set_test_proxy(config: Config, use_proxy: bool) -> Config: + # Set proxy to test in CI. + import os + if use_proxy and (proxy := os.environ.get('CI_WEB_PROXY')): + config1 = deepcopy(config) + config1['exchange']['ccxt_config'] = { + "httpsProxy": proxy, + } + return config1 + + return config + + +@pytest.fixture(params=EXCHANGES, scope="class") +def exchange(request, exchange_conf): + exchange_conf = set_test_proxy( + exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False)) + exchange_conf['exchange']['name'] = request.param + exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency'] + exchange = ExchangeResolver.load_exchange(exchange_conf, validate=True) + + yield exchange, request.param + + +@pytest.fixture(params=EXCHANGES, scope="class") +def exchange_futures(request, exchange_conf, class_mocker): + if EXCHANGES[request.param].get('futures') is not True: + pytest.skip(f"Exchange {request.param} does not support futures.") + else: + exchange_conf = set_test_proxy( + exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False)) + exchange_conf = deepcopy(exchange_conf) + exchange_conf['exchange']['name'] = request.param + exchange_conf['trading_mode'] = 'futures' + exchange_conf['margin_mode'] = 'isolated' + exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency'] + + class_mocker.patch( + 'freqtrade.exchange.binance.Binance.fill_leverage_tiers') + class_mocker.patch(f'{EXMS}.fetch_trading_fees') + class_mocker.patch('freqtrade.exchange.okx.Okx.additional_exchange_init') + class_mocker.patch('freqtrade.exchange.binance.Binance.additional_exchange_init') + class_mocker.patch('freqtrade.exchange.bybit.Bybit.additional_exchange_init') + class_mocker.patch(f'{EXMS}.load_cached_leverage_tiers', return_value=None) + class_mocker.patch(f'{EXMS}.cache_leverage_tiers') + + exchange = ExchangeResolver.load_exchange( + exchange_conf, validate=True, load_leverage_tiers=True) + + yield exchange, request.param diff --git a/tests/exchange_online/test_ccxt_compat.py b/tests/exchange_online/test_ccxt_compat.py index 0aa59db2a..e33875416 100644 --- a/tests/exchange_online/test_ccxt_compat.py +++ b/tests/exchange_online/test_ccxt_compat.py @@ -5,338 +5,14 @@ However, these tests should give a good idea to determine if a new exchange is suitable to run with freqtrade. """ -from copy import deepcopy from datetime import datetime, timedelta, timezone -from pathlib import Path -from typing import Tuple import pytest -from freqtrade.constants import Config from freqtrade.enums import CandleType from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date -from freqtrade.exchange.exchange import Exchange, timeframe_to_msecs -from freqtrade.resolvers.exchange_resolver import ExchangeResolver -from tests.conftest import EXMS, get_default_conf_usdt - - -EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str] - -# Exchanges that should be tested -EXCHANGES = { - 'bittrex': { - 'pair': 'BTC/USDT', - 'stake_currency': 'USDT', - 'hasQuoteVolume': False, - 'timeframe': '1h', - 'leverage_tiers_public': False, - 'leverage_in_spot_market': False, - }, - 'binance': { - 'pair': 'BTC/USDT', - 'stake_currency': 'USDT', - 'use_ci_proxy': True, - 'hasQuoteVolume': True, - 'timeframe': '1h', - 'futures': True, - 'futures_pair': 'BTC/USDT:USDT', - 'hasQuoteVolumeFutures': True, - 'leverage_tiers_public': False, - 'leverage_in_spot_market': False, - 'trades_lookback_hours': 4, - 'private_methods': [ - 'fapiPrivateGetPositionSideDual', - 'fapiPrivateGetMultiAssetsMargin' - ], - 'sample_order': [{ - "symbol": "SOLUSDT", - "orderId": 3551312894, - "orderListId": -1, - "clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba", - "transactTime": 1674493798550, - "price": "15.50000000", - "origQty": "1.10000000", - "executedQty": "0.00000000", - "cummulativeQuoteQty": "0.00000000", - "status": "NEW", - "timeInForce": "GTC", - "type": "LIMIT", - "side": "BUY", - "workingTime": 1674493798550, - "fills": [], - "selfTradePreventionMode": "NONE", - }] - }, - 'binanceus': { - 'pair': 'BTC/USDT', - 'stake_currency': 'USDT', - 'hasQuoteVolume': True, - 'timeframe': '1h', - 'futures': False, - 'sample_order': [{ - "symbol": "SOLUSDT", - "orderId": 3551312894, - "orderListId": -1, - "clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba", - "transactTime": 1674493798550, - "price": "15.50000000", - "origQty": "1.10000000", - "executedQty": "0.00000000", - "cummulativeQuoteQty": "0.00000000", - "status": "NEW", - "timeInForce": "GTC", - "type": "LIMIT", - "side": "BUY", - "workingTime": 1674493798550, - "fills": [], - "selfTradePreventionMode": "NONE", - }] - }, - 'kraken': { - 'pair': 'BTC/USD', - 'stake_currency': 'USD', - 'hasQuoteVolume': True, - 'timeframe': '1h', - 'leverage_tiers_public': False, - 'leverage_in_spot_market': True, - 'trades_lookback_hours': 12, - }, - 'kucoin': { - 'pair': 'XRP/USDT', - 'stake_currency': 'USDT', - 'hasQuoteVolume': True, - 'timeframe': '1h', - 'leverage_tiers_public': False, - 'leverage_in_spot_market': True, - 'sample_order': [ - {'id': '63d6742d0adc5570001d2bbf7'}, # create order - { - 'id': '63d6742d0adc5570001d2bbf7', - 'symbol': 'SOL-USDT', - 'opType': 'DEAL', - 'type': 'limit', - 'side': 'buy', - 'price': '15.5', - 'size': '1.1', - 'funds': '0', - 'dealFunds': '17.05', - 'dealSize': '1.1', - 'fee': '0.000065252', - 'feeCurrency': 'USDT', - 'stp': '', - 'stop': '', - 'stopTriggered': False, - 'stopPrice': '0', - 'timeInForce': 'GTC', - 'postOnly': False, - 'hidden': False, - 'iceberg': False, - 'visibleSize': '0', - 'cancelAfter': 0, - 'channel': 'API', - 'clientOid': '0a053870-11bf-41e5-be61-b272a4cb62e1', - 'remark': None, - 'tags': 'partner:ccxt', - 'isActive': False, - 'cancelExist': False, - 'createdAt': 1674493798550, - 'tradeType': 'TRADE' - }], - }, - 'gate': { - 'pair': 'BTC/USDT', - 'stake_currency': 'USDT', - 'hasQuoteVolume': True, - 'timeframe': '1h', - 'futures': True, - 'futures_pair': 'BTC/USDT:USDT', - 'hasQuoteVolumeFutures': True, - 'leverage_tiers_public': True, - 'leverage_in_spot_market': True, - 'sample_order': [ - { - "id": "276266139423", - "text": "apiv4", - "create_time": "1674493798", - "update_time": "1674493798", - "create_time_ms": "1674493798550", - "update_time_ms": "1674493798550", - "status": "closed", - "currency_pair": "SOL_USDT", - "type": "limit", - "account": "spot", - "side": "buy", - "amount": "1.1", - "price": "15.5", - "time_in_force": "gtc", - "iceberg": "0", - "left": "0", - "fill_price": "17.05", - "filled_total": "17.05", - "avg_deal_price": "15.5", - "fee": "0.0000018", - "fee_currency": "SOL", - "point_fee": "0", - "gt_fee": "0", - "gt_maker_fee": "0", - "gt_taker_fee": "0.0015", - "gt_discount": True, - "rebated_fee": "0", - "rebated_fee_currency": "USDT" - }, - { - # market order - 'id': '276401180529', - 'text': 'apiv4', - 'create_time': '1674493798', - 'update_time': '1674493798', - 'create_time_ms': '1674493798550', - 'update_time_ms': '1674493798550', - 'status': 'cancelled', - 'currency_pair': 'SOL_USDT', - 'type': 'market', - 'account': 'spot', - 'side': 'buy', - 'amount': '17.05', - 'price': '0', - 'time_in_force': 'ioc', - 'iceberg': '0', - 'left': '0.0000000016228', - 'fill_price': '17.05', - 'filled_total': '17.05', - 'avg_deal_price': '15.5', - 'fee': '0', - 'fee_currency': 'SOL', - 'point_fee': '0.0199999999967544', - 'gt_fee': '0', - 'gt_maker_fee': '0', - 'gt_taker_fee': '0', - 'gt_discount': False, - 'rebated_fee': '0', - 'rebated_fee_currency': 'USDT' - } - ], - }, - 'okx': { - 'pair': 'BTC/USDT', - 'stake_currency': 'USDT', - 'hasQuoteVolume': True, - 'timeframe': '1h', - 'futures': True, - 'futures_pair': 'BTC/USDT:USDT', - 'hasQuoteVolumeFutures': False, - 'leverage_tiers_public': True, - 'leverage_in_spot_market': True, - 'private_methods': ['fetch_accounts'], - }, - 'bybit': { - 'pair': 'BTC/USDT', - 'stake_currency': 'USDT', - 'hasQuoteVolume': True, - 'use_ci_proxy': True, - 'timeframe': '1h', - 'futures_pair': 'BTC/USDT:USDT', - 'futures': True, - 'leverage_tiers_public': True, - 'leverage_in_spot_market': True, - 'sample_order': [ - { - "orderId": "1274754916287346280", - "orderLinkId": "1666798627015730", - "symbol": "SOLUSDT", - "createTime": "1674493798550", - "orderPrice": "15.5", - "orderQty": "1.1", - "orderType": "LIMIT", - "side": "BUY", - "status": "NEW", - "timeInForce": "GTC", - "accountId": "5555555", - "execQty": "0", - "orderCategory": "0" - } - ] - }, - 'huobi': { - 'pair': 'ETH/BTC', - 'stake_currency': 'BTC', - 'hasQuoteVolume': True, - 'timeframe': '1h', - 'futures': False, - }, - 'bitvavo': { - 'pair': 'BTC/EUR', - 'stake_currency': 'EUR', - 'hasQuoteVolume': True, - 'timeframe': '1h', - 'leverage_tiers_public': False, - 'leverage_in_spot_market': False, - }, -} - - -@pytest.fixture(scope="class") -def exchange_conf(): - config = get_default_conf_usdt((Path(__file__).parent / "testdata").resolve()) - config['exchange']['pair_whitelist'] = [] - config['exchange']['key'] = '' - config['exchange']['secret'] = '' - config['dry_run'] = False - config['entry_pricing']['use_order_book'] = True - config['exit_pricing']['use_order_book'] = True - return config - - -def set_test_proxy(config: Config, use_proxy: bool) -> Config: - # Set proxy to test in CI. - import os - if use_proxy and (proxy := os.environ.get('CI_WEB_PROXY')): - config1 = deepcopy(config) - config1['exchange']['ccxt_config'] = { - "httpsProxy": proxy, - } - return config1 - - return config - - -@pytest.fixture(params=EXCHANGES, scope="class") -def exchange(request, exchange_conf): - exchange_conf = set_test_proxy( - exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False)) - exchange_conf['exchange']['name'] = request.param - exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency'] - exchange = ExchangeResolver.load_exchange(exchange_conf, validate=True) - - yield exchange, request.param - - -@pytest.fixture(params=EXCHANGES, scope="class") -def exchange_futures(request, exchange_conf, class_mocker): - if EXCHANGES[request.param].get('futures') is not True: - pytest.skip(f"Exchange {request.param} does not support futures.") - else: - exchange_conf = set_test_proxy( - exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False)) - exchange_conf = deepcopy(exchange_conf) - exchange_conf['exchange']['name'] = request.param - exchange_conf['trading_mode'] = 'futures' - exchange_conf['margin_mode'] = 'isolated' - exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency'] - - class_mocker.patch( - 'freqtrade.exchange.binance.Binance.fill_leverage_tiers') - class_mocker.patch(f'{EXMS}.fetch_trading_fees') - class_mocker.patch('freqtrade.exchange.okx.Okx.additional_exchange_init') - class_mocker.patch('freqtrade.exchange.binance.Binance.additional_exchange_init') - class_mocker.patch('freqtrade.exchange.bybit.Bybit.additional_exchange_init') - class_mocker.patch(f'{EXMS}.load_cached_leverage_tiers', return_value=None) - class_mocker.patch(f'{EXMS}.cache_leverage_tiers') - - exchange = ExchangeResolver.load_exchange( - exchange_conf, validate=True, load_leverage_tiers=True) - - yield exchange, request.param +from freqtrade.exchange.exchange import timeframe_to_msecs +from tests.exchange_online.conftest import EXCHANGE_FIXTURE_TYPE, EXCHANGES @pytest.mark.longrun From 6ce08548fb7811b07d5b9a033e9448dcf7bba676 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Aug 2023 18:11:02 +0200 Subject: [PATCH 066/116] Further update ccxt test fixtures --- tests/exchange_online/conftest.py | 39 +++++++++++++++++-------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/tests/exchange_online/conftest.py b/tests/exchange_online/conftest.py index f4b50778d..2d72023f2 100644 --- a/tests/exchange_online/conftest.py +++ b/tests/exchange_online/conftest.py @@ -290,29 +290,24 @@ def set_test_proxy(config: Config, use_proxy: bool) -> Config: return config -@pytest.fixture(params=EXCHANGES, scope="class") -def exchange(request, exchange_conf): +def get_exchange(exchange_name, exchange_conf): exchange_conf = set_test_proxy( - exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False)) - exchange_conf['exchange']['name'] = request.param - exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency'] - exchange = ExchangeResolver.load_exchange(exchange_conf, validate=True) + exchange_conf, EXCHANGES[exchange_name].get('use_ci_proxy', False)) + exchange_conf['exchange']['name'] = exchange_name + exchange_conf['stake_currency'] = EXCHANGES[exchange_name]['stake_currency'] + exchange = ExchangeResolver.load_exchange(exchange_conf, validate=True, load_leverage_tiers=True) - yield exchange, request.param + yield exchange, exchange_name -@pytest.fixture(params=EXCHANGES, scope="class") -def exchange_futures(request, exchange_conf, class_mocker): - if EXCHANGES[request.param].get('futures') is not True: - pytest.skip(f"Exchange {request.param} does not support futures.") +def get_futures_exchange(exchange_name, exchange_conf, class_mocker): + if EXCHANGES[exchange_name].get('futures') is not True: + pytest.skip(f"Exchange {exchange_name} does not support futures.") else: exchange_conf = set_test_proxy( - exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False)) - exchange_conf = deepcopy(exchange_conf) - exchange_conf['exchange']['name'] = request.param + exchange_conf, EXCHANGES[exchange_name].get('use_ci_proxy', False)) exchange_conf['trading_mode'] = 'futures' exchange_conf['margin_mode'] = 'isolated' - exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency'] class_mocker.patch( 'freqtrade.exchange.binance.Binance.fill_leverage_tiers') @@ -323,7 +318,15 @@ def exchange_futures(request, exchange_conf, class_mocker): class_mocker.patch(f'{EXMS}.load_cached_leverage_tiers', return_value=None) class_mocker.patch(f'{EXMS}.cache_leverage_tiers') - exchange = ExchangeResolver.load_exchange( - exchange_conf, validate=True, load_leverage_tiers=True) + yield from get_exchange(exchange_name, exchange_conf) - yield exchange, request.param + +@pytest.fixture(params=EXCHANGES, scope="class") +def exchange(request, exchange_conf): + yield from get_exchange(request.param, exchange_conf) + + +@pytest.fixture(params=EXCHANGES, scope="class") +def exchange_futures(request, exchange_conf, class_mocker): + + yield from get_futures_exchange(request.param, exchange_conf, class_mocker) From 716b1cd002849d9ece0fad65b3e9a60c58790089 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Aug 2023 20:05:21 +0200 Subject: [PATCH 067/116] Improve ccxt tests --- tests/exchange_online/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/exchange_online/conftest.py b/tests/exchange_online/conftest.py index 2d72023f2..45da16dd5 100644 --- a/tests/exchange_online/conftest.py +++ b/tests/exchange_online/conftest.py @@ -295,7 +295,8 @@ def get_exchange(exchange_name, exchange_conf): exchange_conf, EXCHANGES[exchange_name].get('use_ci_proxy', False)) exchange_conf['exchange']['name'] = exchange_name exchange_conf['stake_currency'] = EXCHANGES[exchange_name]['stake_currency'] - exchange = ExchangeResolver.load_exchange(exchange_conf, validate=True, load_leverage_tiers=True) + exchange = ExchangeResolver.load_exchange(exchange_conf, validate=True, + load_leverage_tiers=True) yield exchange, exchange_name @@ -304,6 +305,7 @@ def get_futures_exchange(exchange_name, exchange_conf, class_mocker): if EXCHANGES[exchange_name].get('futures') is not True: pytest.skip(f"Exchange {exchange_name} does not support futures.") else: + exchange_conf = deepcopy(exchange_conf) exchange_conf = set_test_proxy( exchange_conf, EXCHANGES[exchange_name].get('use_ci_proxy', False)) exchange_conf['trading_mode'] = 'futures' From 72bd4e816d78a0f8976cb8d42e0a4d8f7ba3f537 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Aug 2023 16:10:37 +0200 Subject: [PATCH 068/116] Simplify code, no longer log "could not find rate" closes #9031 --- freqtrade/exchange/exchange.py | 4 ++-- freqtrade/rpc/rpc.py | 18 +++++++----------- tests/exchange/test_exchange.py | 2 +- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index afbcff3b6..3f36bd16c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -568,7 +568,7 @@ class Exchange: for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]: if pair in self.markets and self.markets[pair].get('active'): return pair - raise ExchangeError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") + raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") def validate_timeframes(self, timeframe: Optional[str]) -> None: """ @@ -1864,7 +1864,7 @@ class Exchange: tick = self.fetch_ticker(comb) fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask') - except ExchangeError: + except (ValueError, ExchangeError): fee_to_quote_rate = self._config['exchange'].get('unknown_fee_rate', None) if not fee_to_quote_rate: return None diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 466c99aa1..61f4384b7 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -605,17 +605,13 @@ class RPC: est_stake = balance.free est_bot_stake = amount else: - try: - pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) - rate: Optional[float] = tickers.get(pair, {}).get('last', None) - if rate: - if pair.startswith(stake_currency) and not pair.endswith(stake_currency): - rate = 1.0 / rate - est_stake = rate * balance.total - est_bot_stake = rate * amount - except (ExchangeError): - logger.warning(f"Could not get rate for pair {coin}.") - raise ValueError() + pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) + rate: Optional[float] = tickers.get(pair, {}).get('last', None) + if rate: + if pair.startswith(stake_currency) and not pair.endswith(stake_currency): + rate = 1.0 / rate + est_stake = rate * balance.total + est_bot_stake = rate * amount return est_stake, est_bot_stake diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 85d30a9cd..236576747 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3522,7 +3522,7 @@ def test_get_valid_pair_combination(default_conf, mocker, markets): assert ex.get_valid_pair_combination("ETH", "BTC") == "ETH/BTC" assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC" - with pytest.raises(DependencyException, match=r"Could not combine.* to get a valid pair."): + with pytest.raises(ValueError, match=r"Could not combine.* to get a valid pair."): ex.get_valid_pair_combination("NOPAIR", "ETH") From 1ca3cd086f4718b70312fcfbf0cb56511e28ec3a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Aug 2023 10:32:37 +0200 Subject: [PATCH 069/116] Fix missing . in interface docs --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 0f848130f..94e204b54 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -381,7 +381,7 @@ class IStrategy(ABC, HyperStrategyMixin): For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ - When not implemented by a strategy, returns the initial stoploss value + When not implemented by a strategy, returns the initial stoploss value. Only called when use_custom_stoploss is set to True. :param pair: Pair that's currently analyzed From 3ecaedb7d82cdd1f5b6128dddab371417dbd5981 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Aug 2023 11:11:10 +0200 Subject: [PATCH 070/116] use FormatStrings in trade_model --- freqtrade/persistence/trade_model.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index ab170d307..20f6b813e 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -746,10 +746,8 @@ class LocalTrade: self.open_order_id = None self.recalc_trade_from_orders(is_closing=True) if show_msg: - logger.info( - 'Marking %s as closed as the trade is fulfilled and found no open orders for it.', - self - ) + logger.info(f"Marking {self} as closed as the trade is fulfilled " + "and found no open orders for it.") def update_fee(self, fee_cost: float, fee_currency: Optional[str], fee_rate: Optional[float], side: str) -> None: @@ -1192,7 +1190,7 @@ class LocalTrade: Adjust initial Stoploss to desired stoploss for all open trades. """ for trade in Trade.get_open_trades(): - logger.info("Found open trade: %s", trade) + logger.info(f"Found open trade: {trade}") # skip case if trailing-stop changed the stoploss already. if (trade.stop_loss == trade.initial_stop_loss From 2c5a7ceab5d86b0e12b806f76147d139a37ecbaa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Aug 2023 13:21:46 +0200 Subject: [PATCH 071/116] Improve typing of stoploss reinit --- freqtrade/persistence/trade_model.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 20f6b813e..ffc1fc96b 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1185,10 +1185,11 @@ class LocalTrade: return LocalTrade.bt_open_open_trade_count @staticmethod - def stoploss_reinitialization(desired_stoploss): + def stoploss_reinitialization(desired_stoploss: float): """ Adjust initial Stoploss to desired stoploss for all open trades. """ + trade: Trade for trade in Trade.get_open_trades(): logger.info(f"Found open trade: {trade}") @@ -1199,7 +1200,7 @@ class LocalTrade: logger.info(f"Stoploss for {trade} needs adjustment...") # Force reset of stoploss - trade.stop_loss = None + trade.stop_loss = 0.0 trade.initial_stop_loss_pct = None trade.adjust_stop_loss(trade.open_rate, desired_stoploss) logger.info(f"New stoploss: {trade.stop_loss}.") From 90e41274e019e1dd19f4fe4a58b0b5a5861c975a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 03:36:03 +0000 Subject: [PATCH 072/116] Bump pypa/gh-action-pypi-publish from 1.8.8 to 1.8.10 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.8 to 1.8.10. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.8...v1.8.10) --- 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 f4b550855..d187e650b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -461,7 +461,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@v1.8.8 + uses: pypa/gh-action-pypi-publish@v1.8.10 if: (github.event_name == 'release') with: user: __token__ @@ -469,7 +469,7 @@ jobs: repository_url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.8 + uses: pypa/gh-action-pypi-publish@v1.8.10 if: (github.event_name == 'release') with: user: __token__ From 6768641bac0700aa5d422c2a1ebfc6b48c8f5273 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 03:52:31 +0000 Subject: [PATCH 073/116] Bump orjson from 3.9.3 to 3.9.4 Bumps [orjson](https://github.com/ijl/orjson) from 3.9.3 to 3.9.4. - [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.9.3...3.9.4) --- 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 d0f61d2ab..5f6c46d2a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,7 @@ py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.10 # Properly format api responses -orjson==3.9.3 +orjson==3.9.4 # Notify systemd sdnotify==0.3.2 From 4664cc23ebb469eae04b89ab09e306908bc837ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 03:52:34 +0000 Subject: [PATCH 074/116] Bump questionary from 1.10.0 to 2.0.0 Bumps [questionary](https://github.com/tmbo/questionary) from 1.10.0 to 2.0.0. - [Commits](https://github.com/tmbo/questionary/compare/1.10.0...2.0.0) --- updated-dependencies: - dependency-name: questionary 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 d0f61d2ab..f7c241bf1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -49,7 +49,7 @@ psutil==5.9.5 # Support for colorized terminal output colorama==0.4.6 # Building config files interactively -questionary==1.10.0 +questionary==2.0.0 prompt-toolkit==3.0.39 # Extensions to datetime library python-dateutil==2.8.2 From 4942d01574de400df4d598677eeeaeb5c9a9b134 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 03:52:40 +0000 Subject: [PATCH 075/116] Bump mypy from 1.4.1 to 1.5.0 Bumps [mypy](https://github.com/python/mypy) from 1.4.1 to 1.5.0. - [Commits](https://github.com/python/mypy/compare/v1.4.1...v1.5.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 9d1873f2e..f2be35f91 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ coveralls==3.3.1 ruff==0.0.282 -mypy==1.4.1 +mypy==1.5.0 pre-commit==3.3.3 pytest==7.4.0 pytest-asyncio==0.21.1 From f0a50b1f2dfefbce8c3dfafe3f13648f07910564 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 03:52:45 +0000 Subject: [PATCH 076/116] Bump jsonschema from 4.18.6 to 4.19.0 Bumps [jsonschema](https://github.com/python-jsonschema/jsonschema) from 4.18.6 to 4.19.0. - [Release notes](https://github.com/python-jsonschema/jsonschema/releases) - [Changelog](https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst) - [Commits](https://github.com/python-jsonschema/jsonschema/compare/v4.18.6...v4.19.0) --- updated-dependencies: - dependency-name: jsonschema 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 d0f61d2ab..aa3b2f4c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ arrow==1.2.3 cachetools==5.3.1 requests==2.31.0 urllib3==2.0.4 -jsonschema==4.18.6 +jsonschema==4.19.0 TA-Lib==0.4.27 technical==1.4.0 tabulate==0.9.0 From 6e79c19fe06e5b5290c455a96508fbab3852e8e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 03:52:50 +0000 Subject: [PATCH 077/116] Bump tensorboard from 2.13.0 to 2.14.0 Bumps [tensorboard](https://github.com/tensorflow/tensorboard) from 2.13.0 to 2.14.0. - [Release notes](https://github.com/tensorflow/tensorboard/releases) - [Changelog](https://github.com/tensorflow/tensorboard/blob/master/RELEASE.md) - [Commits](https://github.com/tensorflow/tensorboard/compare/2.13.0...2.14.0) --- updated-dependencies: - dependency-name: tensorboard dependency-type: direct:production update-type: version-update:semver-minor ... 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 325b92544..f6a04b319 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -8,5 +8,5 @@ joblib==1.3.1 catboost==1.2; 'arm' not in platform_machine lightgbm==4.0.0 xgboost==1.7.6 -tensorboard==2.13.0 +tensorboard==2.14.0 datasieve==0.1.7 From ceee57c39c90e0c6342af6a87a00fd91ccda449c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 03:52:53 +0000 Subject: [PATCH 078/116] Bump plotly from 5.15.0 to 5.16.0 Bumps [plotly](https://github.com/plotly/plotly.py) from 5.15.0 to 5.16.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.15.0...v5.16.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 72303efcb..f7d979814 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.15.0 +plotly==5.16.0 From 987a7ddac0d9b61e53171d9d813a682831d14c15 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 03:53:17 +0000 Subject: [PATCH 079/116] Bump aiofiles from 23.1.0 to 23.2.1 Bumps [aiofiles](https://github.com/Tinche/aiofiles) from 23.1.0 to 23.2.1. - [Release notes](https://github.com/Tinche/aiofiles/releases) - [Commits](https://github.com/Tinche/aiofiles/compare/v23.1.0...v23.2.1) --- updated-dependencies: - dependency-name: aiofiles 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 d0f61d2ab..e98a0ae93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,7 +43,7 @@ fastapi==0.101.0 pydantic==1.10.11 uvicorn==0.23.2 pyjwt==2.8.0 -aiofiles==23.1.0 +aiofiles==23.2.1 psutil==5.9.5 # Support for colorized terminal output From 9a95011b57f188858d3ea1f37317f17aa9595a31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 05:28:56 +0000 Subject: [PATCH 080/116] Bump ruff from 0.0.282 to 0.0.284 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.0.282 to 0.0.284. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.0.282...v0.0.284) --- 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 f2be35f91..f00a84b55 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.282 +ruff==0.0.284 mypy==1.5.0 pre-commit==3.3.3 pytest==7.4.0 From 0abfad55862ff7473a9b2e1de5c6870192527c88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 05:29:48 +0000 Subject: [PATCH 081/116] Bump tqdm from 4.65.0 to 4.66.1 Bumps [tqdm](https://github.com/tqdm/tqdm) from 4.65.0 to 4.66.1. - [Release notes](https://github.com/tqdm/tqdm/releases) - [Commits](https://github.com/tqdm/tqdm/compare/v4.65.0...v4.66.1) --- updated-dependencies: - dependency-name: tqdm dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-freqai-rl.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai-rl.txt b/requirements-freqai-rl.txt index 74c6d4ebe..3a49eed05 100644 --- a/requirements-freqai-rl.txt +++ b/requirements-freqai-rl.txt @@ -8,4 +8,4 @@ gymnasium==0.28.1 stable_baselines3==2.0.0 sb3_contrib>=2.0.0a9 # Progress bar for stable-baselines3 and sb3-contrib -tqdm==4.65.0 +tqdm==4.66.1 From 7224bdf823e577843d0368fc022a7c8fc5629e7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 05:30:14 +0000 Subject: [PATCH 082/116] Bump ccxt from 4.0.50 to 4.0.59 Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.0.50 to 4.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/4.0.50...4.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 66566737f..70349caaf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ numpy==1.24.3; python_version <= '3.8' pandas==2.0.3 pandas-ta==0.3.14b -ccxt==4.0.50 +ccxt==4.0.59 cryptography==41.0.3; platform_machine != 'armv7l' cryptography==40.0.1; platform_machine == 'armv7l' aiohttp==3.8.5 From bb5277812664e2ec1d830d9bcb98c3f15d43498e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 05:42:41 +0000 Subject: [PATCH 083/116] Bump joblib from 1.3.1 to 1.3.2 Bumps [joblib](https://github.com/joblib/joblib) from 1.3.1 to 1.3.2. - [Release notes](https://github.com/joblib/joblib/releases) - [Changelog](https://github.com/joblib/joblib/blob/master/CHANGES.rst) - [Commits](https://github.com/joblib/joblib/compare/1.3.1...1.3.2) --- updated-dependencies: - dependency-name: joblib dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-freqai.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index f6a04b319..d8421e968 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -4,7 +4,7 @@ # Required for freqai scikit-learn==1.1.3 -joblib==1.3.1 +joblib==1.3.2 catboost==1.2; 'arm' not in platform_machine lightgbm==4.0.0 xgboost==1.7.6 diff --git a/requirements.txt b/requirements.txt index 66566737f..0aad1930c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ pycoingecko==3.1.0 jinja2==3.1.2 tables==3.8.0 blosc==1.11.1 -joblib==1.3.1 +joblib==1.3.2 rich==13.5.2 pyarrow==12.0.1; platform_machine != 'armv7l' From 9b6654e81aa16fe431b1f25ac16609431d35e100 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Aug 2023 09:11:19 +0200 Subject: [PATCH 084/116] Fix ruff E721 (type comparison) --- freqtrade/freqai/data_drawer.py | 2 +- tests/strategy/test_default_strategy.py | 12 ++++++------ tests/utils/test_datetime_helpers.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index b9854919d..b6ded83b1 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -375,7 +375,7 @@ class FreqaiDataDrawer: num_keep = self.freqai_info["purge_old_models"] if not num_keep: return - elif type(num_keep) == bool: + elif isinstance(num_keep, bool): num_keep = 2 model_folders = [x for x in self.full_path.iterdir() if x.is_dir()] diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index 5f41177eb..b5b07e0cd 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -25,13 +25,13 @@ def test_strategy_test_v3(dataframe_1m, fee, is_short, side): strategy = StrategyTestV3({}) metadata = {'pair': 'ETH/BTC'} - assert type(strategy.minimal_roi) is dict - assert type(strategy.stoploss) is float - assert type(strategy.timeframe) is str + assert isinstance(strategy.minimal_roi, dict) + assert isinstance(strategy.stoploss, float) + assert isinstance(strategy.timeframe, str) indicators = strategy.populate_indicators(dataframe_1m, metadata) - assert type(indicators) is DataFrame - assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame - assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame + assert isinstance(indicators, DataFrame) + assert isinstance(strategy.populate_buy_trend(indicators, metadata), DataFrame) + assert isinstance(strategy.populate_sell_trend(indicators, metadata), DataFrame) trade = Trade( open_rate=19_000, diff --git a/tests/utils/test_datetime_helpers.py b/tests/utils/test_datetime_helpers.py index 222410027..6ce975732 100644 --- a/tests/utils/test_datetime_helpers.py +++ b/tests/utils/test_datetime_helpers.py @@ -63,7 +63,7 @@ def test_format_ms_time() -> None: # Date 2018-04-10 18:02:01 date_in_epoch_ms = 1523383321000 date = format_ms_time(date_in_epoch_ms) - assert type(date) is str + assert isinstance(date, str) res = datetime(2018, 4, 10, 18, 2, 1, tzinfo=timezone.utc) assert date == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S') res = datetime(2017, 12, 13, 8, 2, 1, tzinfo=timezone.utc) From 21cf5fc6798a54dff38f22bbab48d027e09715f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Aug 2023 09:11:50 +0200 Subject: [PATCH 085/116] Fix use of string.format() --- tests/exchange/test_binance.py | 2 +- tests/exchange/test_exchange.py | 12 ++++++------ tests/exchange/test_huobi.py | 2 +- tests/exchange/test_kraken.py | 6 +++--- tests/exchange/test_kucoin.py | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 9018d2db9..c4e657ad9 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -35,7 +35,7 @@ def test__get_params_binance(default_conf, mocker, side, type, time_in_force, ex ]) def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expected, side, trademode): api_mock = MagicMock() - order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_id = f'test_prod_buy_{randint(0, 10 ** 6)}' order_type = 'stop_loss_limit' if trademode == TradingMode.SPOT else 'stop' api_mock.create_order = MagicMock(return_value={ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 236576747..84b525cff 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1337,7 +1337,7 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, exchange_name): api_mock = MagicMock() - order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6)) + order_id = f'test_prod_{side}_{randint(0, 10 ** 6)}' api_mock.options = {} if not marketprice else {"createMarketBuyOrderRequiresPrice": True} api_mock.create_order = MagicMock(return_value={ 'id': order_id, @@ -1417,7 +1417,7 @@ def test_buy_dry_run(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_buy_prod(default_conf, mocker, exchange_name): api_mock = MagicMock() - order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_id = f'test_prod_buy_{randint(0, 10 ** 6)}' order_type = 'market' time_in_force = 'gtc' api_mock.options = {} @@ -1506,7 +1506,7 @@ def test_buy_prod(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): api_mock = MagicMock() - order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_id = f'test_prod_buy_{randint(0, 10 ** 6)}' api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, @@ -1573,7 +1573,7 @@ def test_sell_dry_run(default_conf, mocker): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_sell_prod(default_conf, mocker, exchange_name): api_mock = MagicMock() - order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) + order_id = f'test_prod_sell_{randint(0, 10 ** 6)}' order_type = 'market' api_mock.options = {} api_mock.create_order = MagicMock(return_value={ @@ -1651,7 +1651,7 @@ def test_sell_prod(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): api_mock = MagicMock() - order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) + order_id = f'test_prod_sell_{randint(0, 10 ** 6)}' api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'symbol': 'ETH/BTC', @@ -5357,7 +5357,7 @@ def test_get_liquidation_price( ]) def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amount): api_mock = MagicMock() - order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_id = f'test_prod_buy_{randint(0, 10 ** 6)}' api_mock.create_order = MagicMock(return_value={ 'id': order_id, diff --git a/tests/exchange/test_huobi.py b/tests/exchange/test_huobi.py index 8be8ef8b3..b3f3c0900 100644 --- a/tests/exchange/test_huobi.py +++ b/tests/exchange/test_huobi.py @@ -16,7 +16,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers ]) def test_create_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side): api_mock = MagicMock() - order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_id = f'test_prod_buy_{randint(0, 10 ** 6)}' order_type = 'stop-limit' api_mock.create_order = MagicMock(return_value={ diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 8fc23b94e..7db3eeeeb 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -15,7 +15,7 @@ STOPLOSS_LIMIT_ORDERTYPE = 'stop-loss-limit' def test_buy_kraken_trading_agreement(default_conf, mocker): api_mock = MagicMock() - order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_id = f'test_prod_buy_{randint(0, 10 ** 6)}' order_type = 'limit' time_in_force = 'ioc' api_mock.options = {} @@ -56,7 +56,7 @@ def test_buy_kraken_trading_agreement(default_conf, mocker): def test_sell_kraken_trading_agreement(default_conf, mocker): api_mock = MagicMock() - order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) + order_id = f'test_prod_sell_{randint(0, 10 ** 6)}' order_type = 'market' api_mock.options = {} api_mock.create_order = MagicMock(return_value={ @@ -181,7 +181,7 @@ def test_get_balances_prod(default_conf, mocker): ]) def test_create_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedprice): api_mock = MagicMock() - order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_id = f'test_prod_buy_{randint(0, 10 ** 6)}' api_mock.create_order = MagicMock(return_value={ 'id': order_id, diff --git a/tests/exchange/test_kucoin.py b/tests/exchange/test_kucoin.py index 741ee27be..a74b77859 100644 --- a/tests/exchange/test_kucoin.py +++ b/tests/exchange/test_kucoin.py @@ -17,7 +17,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers ]) def test_create_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side, order_type): api_mock = MagicMock() - order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_id = f'test_prod_buy_{randint(0, 10 ** 6)}' api_mock.create_order = MagicMock(return_value={ 'id': order_id, @@ -136,7 +136,7 @@ def test_stoploss_adjust_kucoin(mocker, default_conf): ]) def test_kucoin_create_order(default_conf, mocker, side, ordertype, rate): api_mock = MagicMock() - order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6)) + order_id = f'test_prod_{side}_{randint(0, 10 ** 6)}' api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { From f4844c122468bd6034006a6dd7190653ce17c042 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Aug 2023 09:19:29 +0200 Subject: [PATCH 086/116] Downgrade prompt-toolkid to 3.0.36 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f7c241bf1..0ab1dd8b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -50,7 +50,7 @@ psutil==5.9.5 colorama==0.4.6 # Building config files interactively questionary==2.0.0 -prompt-toolkit==3.0.39 +prompt-toolkit==3.0.36 # Extensions to datetime library python-dateutil==2.8.2 From d7556cd66af276d419cb65e1c296a90dfd1ceeff Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Aug 2023 16:14:40 +0200 Subject: [PATCH 087/116] Remove duplicate call in backtesting --- freqtrade/optimize/backtesting.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ce20d2178..3321b07f3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1137,7 +1137,6 @@ class Backtesting: trade.open_order_id = None sub_trade = order.safe_amount_after_fee != trade.amount if sub_trade: - order.close_bt_order(current_time, trade) trade.recalc_trade_from_orders() else: trade.close_date = current_time From d7e9f87b33b7dbf7d1152e1353168ea4211615f8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Aug 2023 16:16:13 +0200 Subject: [PATCH 088/116] Improve comment indent --- freqtrade/optimize/backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3321b07f3..d054d4ecc 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1126,11 +1126,11 @@ class Backtesting: trade.open_order_id = None self.wallets.update() - # 4. Create exit orders (if any) + # 4. Create exit orders (if any) if not trade.open_order_id: self._check_trade_exit(trade, row) # Place exit order if necessary - # 5. Process exit orders. + # 5. Process exit orders. order = trade.select_order(trade.exit_side, is_open=True) if order and self._get_order_filled(order.ft_price, row): order.close_bt_order(current_time, trade) From bcc2dd98037326c9855f90a550971d04a2cb8c04 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Aug 2023 16:35:27 +0200 Subject: [PATCH 089/116] Simplify backtest order closing --- freqtrade/optimize/backtesting.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d054d4ecc..63151f732 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -568,7 +568,7 @@ class Backtesting: if pos_trade is not None: order = pos_trade.orders[-1] if self._get_order_filled(order.ft_price, row): - order.close_bt_order(current_date, trade) + self._close_open_order(order, trade, current_date, row) trade.recalc_trade_from_orders() self.wallets.update() return pos_trade @@ -579,6 +579,12 @@ class Backtesting: """ Rate is within candle, therefore filled""" return row[LOW_IDX] <= rate <= row[HIGH_IDX] + def _close_open_order( + self, order: Order, trade: LocalTrade, current_date: datetime, row: Tuple) -> None: + """ Close an open order, and update trade accordingly""" + order.close_bt_order(current_date, trade) + trade.open_order_id = None + def _get_exit_for_signal( self, trade: LocalTrade, row: Tuple, exit_: ExitCheckTuple, amount: Optional[float] = None) -> Optional[LocalTrade]: @@ -903,8 +909,8 @@ class Backtesting: ) order._trade_bt = trade trade.orders.append(order) - if pos_adjust and self._get_order_filled(order.ft_price, row): - order.close_bt_order(current_time, trade) + if self._get_order_filled(order.ft_price, row): + self._close_open_order(order, trade, current_time, row) else: trade.open_order_id = str(self.order_id_counter) trade.recalc_trade_from_orders() @@ -1122,8 +1128,7 @@ class Backtesting: # 3. Process entry orders. order = trade.select_order(trade.entry_side, is_open=True) if order and self._get_order_filled(order.ft_price, row): - order.close_bt_order(current_time, trade) - trade.open_order_id = None + self._close_open_order(order, trade, current_time, row) self.wallets.update() # 4. Create exit orders (if any) @@ -1133,8 +1138,7 @@ class Backtesting: # 5. Process exit orders. order = trade.select_order(trade.exit_side, is_open=True) if order and self._get_order_filled(order.ft_price, row): - order.close_bt_order(current_time, trade) - trade.open_order_id = None + self._close_open_order(order, trade, current_time, row) sub_trade = order.safe_amount_after_fee != trade.amount if sub_trade: trade.recalc_trade_from_orders() From 08bc6158268f7db4ceabd299e11ed6c5eb3c4a78 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Aug 2023 16:47:26 +0200 Subject: [PATCH 090/116] Further simplify backtest order handling --- freqtrade/optimize/backtesting.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 63151f732..9836722fd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -567,8 +567,7 @@ class Backtesting: pos_trade = self._get_exit_for_signal(trade, row, exit_, amount) if pos_trade is not None: order = pos_trade.orders[-1] - if self._get_order_filled(order.ft_price, row): - self._close_open_order(order, trade, current_date, row) + if self._try_close_open_order(order, trade, current_date, row): trade.recalc_trade_from_orders() self.wallets.update() return pos_trade @@ -579,11 +578,18 @@ class Backtesting: """ Rate is within candle, therefore filled""" return row[LOW_IDX] <= rate <= row[HIGH_IDX] - def _close_open_order( - self, order: Order, trade: LocalTrade, current_date: datetime, row: Tuple) -> None: - """ Close an open order, and update trade accordingly""" - order.close_bt_order(current_date, trade) - trade.open_order_id = None + def _try_close_open_order( + self, order: Optional[Order], trade: LocalTrade, current_date: datetime, + row: Tuple) -> bool: + """ + Check if an order is open and if it should've filled. + :return: True if the order filled. + """ + if order and self._get_order_filled(order.ft_price, row): + order.close_bt_order(current_date, trade) + trade.open_order_id = None + return True + return False def _get_exit_for_signal( self, trade: LocalTrade, row: Tuple, exit_: ExitCheckTuple, @@ -909,9 +915,7 @@ class Backtesting: ) order._trade_bt = trade trade.orders.append(order) - if self._get_order_filled(order.ft_price, row): - self._close_open_order(order, trade, current_time, row) - else: + if not self._try_close_open_order(order, trade, current_time, row): trade.open_order_id = str(self.order_id_counter) trade.recalc_trade_from_orders() @@ -1127,8 +1131,7 @@ class Backtesting: for trade in list(LocalTrade.bt_trades_open_pp[pair]): # 3. Process entry orders. order = trade.select_order(trade.entry_side, is_open=True) - if order and self._get_order_filled(order.ft_price, row): - self._close_open_order(order, trade, current_time, row) + if self._try_close_open_order(order, trade, current_time, row): self.wallets.update() # 4. Create exit orders (if any) @@ -1137,8 +1140,7 @@ class Backtesting: # 5. Process exit orders. order = trade.select_order(trade.exit_side, is_open=True) - if order and self._get_order_filled(order.ft_price, row): - self._close_open_order(order, trade, current_time, row) + if self._try_close_open_order(order, trade, current_time, row): sub_trade = order.safe_amount_after_fee != trade.amount if sub_trade: trade.recalc_trade_from_orders() From d53b6871ea9c6a4fccc022aa8126a86032c4d081 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Aug 2023 07:51:25 +0200 Subject: [PATCH 091/116] Bump pre-commit mypy --- .pre-commit-config.yaml | 2 +- freqtrade/optimize/backtesting.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6e4919763..8fea99cd1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: # stages: [push] - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.3.0" + rev: "v1.5.0" hooks: - id: mypy exclude: build_helpers diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9836722fd..bdd04ba7f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1140,7 +1140,7 @@ class Backtesting: # 5. Process exit orders. order = trade.select_order(trade.exit_side, is_open=True) - if self._try_close_open_order(order, trade, current_time, row): + if order and self._try_close_open_order(order, trade, current_time, row): sub_trade = order.safe_amount_after_fee != trade.amount if sub_trade: trade.recalc_trade_from_orders() From db9247e78ee33f14f46f3e3718fb27597a2797f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Aug 2023 14:54:11 +0200 Subject: [PATCH 092/116] prevent errors in custom stop from crashing the bot --- freqtrade/strategy/interface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 94e204b54..eca3e3ede 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -1181,7 +1181,8 @@ class IStrategy(ABC, HyperStrategyMixin): bound = (low if trade.is_short else high) bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound) if self.use_custom_stoploss and dir_correct: - stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None + stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None, + supress_error=True )(pair=trade.pair, trade=trade, current_time=current_time, current_rate=(bound or current_rate), From d768afed37a39629ada784cfb12b9fb5d5cb3632 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Aug 2023 15:19:34 +0200 Subject: [PATCH 093/116] price_to_precision should only run once --- freqtrade/persistence/trade_model.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index ffc1fc96b..7b17bef8d 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -614,11 +614,9 @@ class LocalTrade: """ Method used internally to set self.stop_loss. """ - stop_loss_norm = price_to_precision(stop_loss, self.price_precision, self.precision_mode, - rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP) if not self.stop_loss: - self.initial_stop_loss = stop_loss_norm - self.stop_loss = stop_loss_norm + self.initial_stop_loss = stop_loss + self.stop_loss = stop_loss self.stop_loss_pct = -1 * abs(percent) @@ -642,26 +640,27 @@ class LocalTrade: else: new_loss = float(current_price * (1 - abs(stoploss / leverage))) + stop_loss_norm = price_to_precision(new_loss, self.price_precision, self.precision_mode, + rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP) # no stop loss assigned yet if self.initial_stop_loss_pct is None or refresh: - self.__set_stop_loss(new_loss, stoploss) + self.__set_stop_loss(stop_loss_norm, stoploss) self.initial_stop_loss = price_to_precision( - new_loss, self.price_precision, self.precision_mode, + stop_loss_norm, self.price_precision, self.precision_mode, rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP) self.initial_stop_loss_pct = -1 * abs(stoploss) # evaluate if the stop loss needs to be updated else: - - higher_stop = new_loss > self.stop_loss - lower_stop = new_loss < self.stop_loss + higher_stop = stop_loss_norm > self.stop_loss + lower_stop = stop_loss_norm < self.stop_loss # stop losses only walk up, never down!, # ? But adding more to a leveraged trade would create a lower liquidation price, # ? decreasing the minimum stoploss if (higher_stop and not self.is_short) or (lower_stop and self.is_short): logger.debug(f"{self.pair} - Adjusting stoploss...") - self.__set_stop_loss(new_loss, stoploss) + self.__set_stop_loss(stop_loss_norm, stoploss) else: logger.debug(f"{self.pair} - Keeping current stoploss...") From a4842113ce7d3999ad54b7513be1b97dfbe05b6f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Aug 2023 06:58:35 +0200 Subject: [PATCH 094/116] Split strategy template to have conditional attributes --- freqtrade/templates/base_strategy.py.j2 | 14 +------------- .../strategy_attributes_full.j2 | 13 +++++++++++++ .../strategy_attributes_minimal.j2 | 0 3 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 freqtrade/templates/strategy_subtemplates/strategy_attributes_full.j2 create mode 100644 freqtrade/templates/strategy_subtemplates/strategy_attributes_minimal.j2 diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 53426b211..a4e0a2b24 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -78,19 +78,7 @@ class {{ strategy }}(IStrategy): buy_rsi = IntParameter(10, 40, default=30, space="buy") sell_rsi = IntParameter(60, 90, default=70, space="sell") - # Optional order type mapping. - order_types = { - 'entry': 'limit', - 'exit': 'limit', - 'stoploss': 'market', - 'stoploss_on_exchange': False - } - - # Optional order time in force. - order_time_in_force = { - 'entry': 'GTC', - 'exit': 'GTC' - } + {{ attributes | indent(4) }} {{ plot_config | indent(4) }} def informative_pairs(self): diff --git a/freqtrade/templates/strategy_subtemplates/strategy_attributes_full.j2 b/freqtrade/templates/strategy_subtemplates/strategy_attributes_full.j2 new file mode 100644 index 000000000..86445510d --- /dev/null +++ b/freqtrade/templates/strategy_subtemplates/strategy_attributes_full.j2 @@ -0,0 +1,13 @@ +# Optional order type mapping. +order_types = { + 'entry': 'limit', + 'exit': 'limit', + 'stoploss': 'market', + 'stoploss_on_exchange': False +} + +# Optional order time in force. +order_time_in_force = { + 'entry': 'GTC', + 'exit': 'GTC' +} diff --git a/freqtrade/templates/strategy_subtemplates/strategy_attributes_minimal.j2 b/freqtrade/templates/strategy_subtemplates/strategy_attributes_minimal.j2 new file mode 100644 index 000000000..e69de29bb From 6b11f3063f90e043a0b52d4261dc2bf36def2af0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Aug 2023 06:58:50 +0200 Subject: [PATCH 095/116] "minimal" strategy templates shouldn't render all attributes --- freqtrade/commands/deploy_commands.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 9ec33eac4..8237a63db 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -35,6 +35,10 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st Deploy new strategy from template to strategy_path """ fallback = 'full' + attributes = render_template_with_fallback( + templatefile=f"strategy_subtemplates/strategy_attributes_{subtemplate}.j2", + templatefallbackfile=f"strategy_subtemplates/strategy_attributes_{fallback}.j2", + ) indicators = render_template_with_fallback( templatefile=f"strategy_subtemplates/indicators_{subtemplate}.j2", templatefallbackfile=f"strategy_subtemplates/indicators_{fallback}.j2", @@ -58,6 +62,7 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st strategy_text = render_template(templatefile='base_strategy.py.j2', arguments={"strategy": strategy_name, + "attributes": attributes, "indicators": indicators, "buy_trend": buy_trend, "sell_trend": sell_trend, From afcaeafd96f5a03d787a2c8632ce64d23d37a5fe Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Aug 2023 07:42:05 +0200 Subject: [PATCH 096/116] Move rendering commands to utils --- freqtrade/commands/build_config_commands.py | 2 +- freqtrade/commands/deploy_commands.py | 2 +- freqtrade/misc.py | 24 -------------------- freqtrade/util/__init__.py | 1 + freqtrade/util/template_renderer.py | 25 +++++++++++++++++++++ 5 files changed, 28 insertions(+), 26 deletions(-) create mode 100644 freqtrade/util/template_renderer.py diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 311622458..3b0c39921 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -10,7 +10,7 @@ from freqtrade.configuration.directory_operations import chown_user_directory from freqtrade.constants import UNLIMITED_STAKE_AMOUNT from freqtrade.exceptions import OperationalException from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges -from freqtrade.misc import render_template +from freqtrade.util import render_template logger = logging.getLogger(__name__) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 8237a63db..75da2552e 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -10,7 +10,7 @@ from freqtrade.configuration.directory_operations import copy_sample_files, crea from freqtrade.constants import USERPATH_STRATEGIES from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException -from freqtrade.misc import render_template, render_template_with_fallback +from freqtrade.util import render_template, render_template_with_fallback logger = logging.getLogger(__name__) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index e715c280a..f8d730fae 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -192,30 +192,6 @@ def plural(num: float, singular: str, plural: Optional[str] = None) -> str: return singular if (num == 1 or num == -1) else plural or singular + 's' -def render_template(templatefile: str, arguments: dict = {}) -> str: - - from jinja2 import Environment, PackageLoader, select_autoescape - - env = Environment( - loader=PackageLoader('freqtrade', 'templates'), - autoescape=select_autoescape(['html', 'xml']) - ) - template = env.get_template(templatefile) - return template.render(**arguments) - - -def render_template_with_fallback(templatefile: str, templatefallbackfile: str, - arguments: dict = {}) -> str: - """ - Use templatefile if possible, otherwise fall back to templatefallbackfile - """ - from jinja2.exceptions import TemplateNotFound - try: - return render_template(templatefile, arguments) - except TemplateNotFound: - return render_template(templatefallbackfile, arguments) - - def chunks(lst: List[Any], n: int) -> Iterator[List[Any]]: """ Split lst into chunks of the size n. diff --git a/freqtrade/util/__init__.py b/freqtrade/util/__init__.py index 92c79b899..af09624ac 100644 --- a/freqtrade/util/__init__.py +++ b/freqtrade/util/__init__.py @@ -2,6 +2,7 @@ from freqtrade.util.datetime_helpers import (dt_floor_day, dt_from_ts, dt_humani dt_utc, format_ms_time, shorten_date) from freqtrade.util.ft_precise import FtPrecise from freqtrade.util.periodic_cache import PeriodicCache +from freqtrade.util.template_renderer import render_template, render_template_with_fallback # noqa __all__ = [ diff --git a/freqtrade/util/template_renderer.py b/freqtrade/util/template_renderer.py new file mode 100644 index 000000000..821b8e458 --- /dev/null +++ b/freqtrade/util/template_renderer.py @@ -0,0 +1,25 @@ +""" +Jinja2 rendering utils, used to generate new strategy and configurations. +""" +def render_template(templatefile: str, arguments: dict = {}) -> str: + + from jinja2 import Environment, PackageLoader, select_autoescape + + env = Environment( + loader=PackageLoader('freqtrade', 'templates'), + autoescape=select_autoescape(['html', 'xml']) + ) + template = env.get_template(templatefile) + return template.render(**arguments) + + +def render_template_with_fallback(templatefile: str, templatefallbackfile: str, + arguments: dict = {}) -> str: + """ + Use templatefile if possible, otherwise fall back to templatefallbackfile + """ + from jinja2.exceptions import TemplateNotFound + try: + return render_template(templatefile, arguments) + except TemplateNotFound: + return render_template(templatefallbackfile, arguments) From 3f5903bad8f5fc1d33da24317ec3e596c0a27de2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Aug 2023 07:42:43 +0200 Subject: [PATCH 097/116] Split tests for jinja utils --- freqtrade/util/template_renderer.py | 2 ++ tests/test_misc.py | 17 +---------------- tests/utils/test_rendering_utils.py | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 16 deletions(-) create mode 100644 tests/utils/test_rendering_utils.py diff --git a/freqtrade/util/template_renderer.py b/freqtrade/util/template_renderer.py index 821b8e458..362d0f875 100644 --- a/freqtrade/util/template_renderer.py +++ b/freqtrade/util/template_renderer.py @@ -1,6 +1,8 @@ """ Jinja2 rendering utils, used to generate new strategy and configurations. """ + + def render_template(templatefile: str, arguments: dict = {}) -> str: from jinja2 import Environment, PackageLoader, select_autoescape diff --git a/tests/test_misc.py b/tests/test_misc.py index 3943e7f15..e94e299fd 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -9,8 +9,7 @@ import pytest from freqtrade.misc import (dataframe_to_json, decimals_per_coin, deep_merge_dicts, file_dump_json, file_load_json, is_file_in_dir, json_to_dataframe, pair_to_filename, - parse_db_uri_for_logging, plural, render_template, - render_template_with_fallback, round_coin_value, safe_value_fallback, + parse_db_uri_for_logging, plural, round_coin_value, safe_value_fallback, safe_value_fallback2) @@ -177,20 +176,6 @@ def test_plural() -> None: assert plural(-1.5, "ox", "oxen") == "oxen" -def test_render_template_fallback(mocker): - from jinja2.exceptions import TemplateNotFound - with pytest.raises(TemplateNotFound): - val = render_template( - templatefile='subtemplates/indicators_does-not-exist.j2',) - - val = render_template_with_fallback( - templatefile='strategy_subtemplates/indicators_does-not-exist.j2', - templatefallbackfile='strategy_subtemplates/indicators_minimal.j2', - ) - assert isinstance(val, str) - assert 'if self.dp' in val - - @pytest.mark.parametrize('conn_url,expected', [ ("postgresql+psycopg2://scott123:scott123@host:1245/dbname", "postgresql+psycopg2://scott123:*****@host:1245/dbname"), diff --git a/tests/utils/test_rendering_utils.py b/tests/utils/test_rendering_utils.py new file mode 100644 index 000000000..7d52b4f26 --- /dev/null +++ b/tests/utils/test_rendering_utils.py @@ -0,0 +1,17 @@ +import pytest + +from freqtrade.util import render_template, render_template_with_fallback + + +def test_render_template_fallback(): + from jinja2.exceptions import TemplateNotFound + with pytest.raises(TemplateNotFound): + val = render_template( + templatefile='subtemplates/indicators_does-not-exist.j2',) + + val = render_template_with_fallback( + templatefile='strategy_subtemplates/indicators_does-not-exist.j2', + templatefallbackfile='strategy_subtemplates/indicators_minimal.j2', + ) + assert isinstance(val, str) + assert 'if self.dp' in val From 09ec00888f7bb3ab359b35422bb850b7b3b2d870 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Aug 2023 16:53:21 +0200 Subject: [PATCH 098/116] Don't use global variable in test --- tests/optimize/test_backtesting.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index f0bd18768..c6e01f0ad 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -1122,10 +1122,10 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi processed = backtesting.strategy.advise_all_indicators(data) min_date, max_date = get_timerange(processed) - global count count = 0 def tmp_confirm_entry(pair, current_time, **kwargs): + nonlocal count dp = backtesting.strategy.dp df, _ = dp.get_analyzed_dataframe(pair, backtesting.strategy.timeframe) current_candle = df.iloc[-1].squeeze() @@ -1135,8 +1135,7 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi assert candle_date == current_time # These asserts don't properly raise as they are nested, # therefore we increment count and assert for that. - global count - count = count + 1 + count += 1 backtesting.strategy.confirm_trade_entry = tmp_confirm_entry backtesting.backtest( From 6f347b839a4f9098493020a15be441db13cb88e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Aug 2023 17:27:21 +0200 Subject: [PATCH 099/116] Remove optionality from timeframe parameter (it was never optional, and code was failing if it wasn't provided). --- freqtrade/data/dataprovider.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index e7ba20c44..88cda07ab 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -284,7 +284,7 @@ class DataProvider: def historic_ohlcv( self, pair: str, - timeframe: Optional[str] = None, + timeframe: str, candle_type: str = '' ) -> DataFrame: """ @@ -307,7 +307,7 @@ class DataProvider: timerange.subtract_start(tf_seconds * startup_candles) self.__cached_pairs_backtesting[saved_pair] = load_pair_history( pair=pair, - timeframe=timeframe or self._config['timeframe'], + timeframe=timeframe, datadir=self._config['datadir'], timerange=timerange, data_format=self._config['dataformat_ohlcv'], @@ -354,6 +354,7 @@ class DataProvider: data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type) else: # Get historical OHLCV data (cached on disk). + timeframe = timeframe or self._config['timeframe'] data = self.historic_ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type) if len(data) == 0: logger.warning(f"No data found for ({pair}, {timeframe}, {candle_type}).") From 161ab14ed0be89fd3163693162a08cd22fd56248 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Aug 2023 17:48:07 +0200 Subject: [PATCH 100/116] Avoid lookahead bias through informative pairs in callbacks --- freqtrade/data/dataprovider.py | 18 +++++++++++++++++- freqtrade/optimize/backtesting.py | 2 ++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 88cda07ab..11cbd7934 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -17,7 +17,7 @@ from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWith from freqtrade.data.history import load_pair_history from freqtrade.enums import CandleType, RPCMessageType, RunMode from freqtrade.exceptions import ExchangeError, OperationalException -from freqtrade.exchange import Exchange, timeframe_to_seconds +from freqtrade.exchange import Exchange, timeframe_to_prev_date, timeframe_to_seconds from freqtrade.exchange.types import OrderBook from freqtrade.misc import append_candles_to_dataframe from freqtrade.rpc import RPCManager @@ -46,6 +46,8 @@ class DataProvider: self.__rpc = rpc self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {} self.__slice_index: Optional[int] = None + self.__slice_date: Optional[datetime] = None + self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {} self.__producer_pairs_df: Dict[str, Dict[PairWithTimeframe, Tuple[DataFrame, datetime]]] = {} @@ -64,10 +66,19 @@ class DataProvider: def _set_dataframe_max_index(self, limit_index: int): """ Limit analyzed dataframe to max specified index. + Only relevant in backtesting. :param limit_index: dataframe index. """ self.__slice_index = limit_index + def _set_dataframe_max_date(self, limit_date: datetime): + """ + Limit infomrative dataframe to max specified index. + Only relevant in backtesting. + :param limit_date: "current date" + """ + self.__slice_date = limit_date + def _set_cached_df( self, pair: str, @@ -356,6 +367,11 @@ class DataProvider: # Get historical OHLCV data (cached on disk). timeframe = timeframe or self._config['timeframe'] data = self.historic_ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type) + # Cut date to timeframe-specific date. + # This is necessary to prevent lookahead bias in callbacks through informative pairs. + if self.__slice_date: + cutoff_date = timeframe_to_prev_date(timeframe, self.__slice_date) + data = data.loc[data['date'] < cutoff_date] if len(data) == 0: logger.warning(f"No data found for ({pair}, {timeframe}, {candle_type}).") return data diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bdd04ba7f..4c941ea3a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1229,12 +1229,14 @@ class Backtesting: is_first = True current_time_det = current_time for det_row in detail_data[HEADERS].values.tolist(): + self.dataprovider._set_dataframe_max_date(current_time_det) open_trade_count_start = self.backtest_loop( det_row, pair, current_time_det, end_date, open_trade_count_start, trade_dir, is_first) current_time_det += timedelta(minutes=self.timeframe_detail_min) is_first = False else: + self.dataprovider._set_dataframe_max_date(current_time) open_trade_count_start = self.backtest_loop( row, pair, current_time, end_date, open_trade_count_start, trade_dir) From 045d8c6fcaaef445d80cb6debc0593626d116a1c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Aug 2023 17:56:40 +0200 Subject: [PATCH 101/116] Add test for informative pair filtering --- tests/data/test_dataprovider.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 4ff4f214b..31c6763bc 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -129,9 +129,14 @@ def test_get_pair_dataframe(mocker, default_conf, ohlcv_history, candle_type): default_conf["runmode"] = RunMode.BACKTEST dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.BACKTEST - assert isinstance(dp.get_pair_dataframe( - "UNITTEST/BTC", timeframe, candle_type=candle_type), DataFrame) - # assert dp.get_pair_dataframe("NONESENSE/AAA", timeframe).empty + df = dp.get_pair_dataframe("UNITTEST/BTC", timeframe, candle_type=candle_type) + assert isinstance(df, DataFrame) + assert len(df) == 3 # ohlcv_history mock has just 3 rows + + dp._set_dataframe_max_date(ohlcv_history.iloc[-1]['date']) + df = dp.get_pair_dataframe("UNITTEST/BTC", timeframe, candle_type=candle_type) + assert isinstance(df, DataFrame) + assert len(df) == 2 # ohlcv_history is limited to 2 rows now def test_available_pairs(mocker, default_conf, ohlcv_history): From bea67822235b473537a5b82b86113845c2d2f319 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Aug 2023 19:33:01 +0200 Subject: [PATCH 102/116] Ensure cutoffs in backtesting are properly tested --- tests/optimize/test_backtesting.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index c6e01f0ad..46a1d5d12 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -20,7 +20,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange from freqtrade.enums import CandleType, ExitType, RunMode from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.exchange.exchange import timeframe_to_next_date +from freqtrade.exchange import timeframe_to_next_date, timeframe_to_prev_date from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename, get_strategy_run_id from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import LocalTrade, Trade @@ -1135,6 +1135,12 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi assert candle_date == current_time # These asserts don't properly raise as they are nested, # therefore we increment count and assert for that. + df = dp.get_pair_dataframe(pair, backtesting.strategy.timeframe) + prior_time = timeframe_to_prev_date(backtesting.strategy.timeframe, + candle_date - timedelta(seconds=1)) + assert prior_time == df.iloc[-1].squeeze()['date'] + assert df.iloc[-1].squeeze()['date'] < current_time + count += 1 backtesting.strategy.confirm_trade_entry = tmp_confirm_entry From 452e1ab0160c8b681e62c6046b8062d35f374e7e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Aug 2023 19:43:04 +0200 Subject: [PATCH 103/116] get_analyzed_dataframe should provide dataframe with startup candles closes #7389 --- freqtrade/optimize/backtesting.py | 10 ++++++---- tests/optimize/test_backtesting.py | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4c941ea3a..21390489e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -369,13 +369,14 @@ class Backtesting: # Cleanup from prior runs pair_data.drop(HEADERS[5:] + ['buy', 'sell'], axis=1, errors='ignore') df_analyzed = self.strategy.ft_advise_signals(pair_data, {'pair': pair}) - # Trim startup period from analyzed dataframe - df_analyzed = processed[pair] = pair_data = trim_dataframe( - df_analyzed, self.timerange, startup_candles=self.required_startup) # Update dataprovider cache self.dataprovider._set_cached_df( pair, self.timeframe, df_analyzed, self.config['candle_type_def']) + # Trim startup period from analyzed dataframe + df_analyzed = processed[pair] = pair_data = trim_dataframe( + df_analyzed, self.timerange, startup_candles=self.required_startup) + # Create a copy of the dataframe before shifting, that way the entry signal/tag # remains on the correct candle for callbacks. df_analyzed = df_analyzed.copy() @@ -1196,7 +1197,8 @@ class Backtesting: row_index += 1 indexes[pair] = row_index - self.dataprovider._set_dataframe_max_index(row_index) + self.dataprovider._set_dataframe_max_index(self.required_startup + row_index) + self.dataprovider._set_dataframe_max_date(current_time) current_detail_time: datetime = row[DATE_IDX].to_pydatetime() trade_dir: Optional[LongShort] = self.check_for_trade_entry(row) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 46a1d5d12..ac409bf71 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -1359,11 +1359,11 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) # Cached data correctly removed amounts offset = 1 if tres == 0 else 0 - removed_candles = len(data[pair]) - offset - backtesting.strategy.startup_candle_count + removed_candles = len(data[pair]) - offset assert len(backtesting.dataprovider.get_analyzed_dataframe(pair, '5m')[0]) == removed_candles assert len( backtesting.dataprovider.get_analyzed_dataframe('NXT/BTC', '5m')[0] - ) == len(data['NXT/BTC']) - 1 - backtesting.strategy.startup_candle_count + ) == len(data['NXT/BTC']) - 1 backtesting.strategy.max_open_trades = 1 backtesting.config.update({'max_open_trades': 1}) From 77c7dd8a126e9cf9b37e061903c90b17e0854fe3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Aug 2023 07:44:19 +0200 Subject: [PATCH 104/116] Add FIAT mapping for true usdt --- freqtrade/rpc/fiat_convert.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index ea13209c4..d790c4b48 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -26,6 +26,7 @@ coingecko_mapping = { 'sol': 'solana', 'usdt': 'tether', 'busd': 'binance-usd', + 'tusd': 'true-usd', } From d9fb40ca3eebf2e94f839544d61fa84ec2536b0d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Aug 2023 07:45:22 +0200 Subject: [PATCH 105/116] Update cached binance leverage tiers --- .../exchange/binance_leverage_tiers.json | 918 +++++++++++++----- 1 file changed, 679 insertions(+), 239 deletions(-) diff --git a/freqtrade/exchange/binance_leverage_tiers.json b/freqtrade/exchange/binance_leverage_tiers.json index 7be7a9d29..a4a81abb3 100644 --- a/freqtrade/exchange/binance_leverage_tiers.json +++ b/freqtrade/exchange/binance_leverage_tiers.json @@ -590,13 +590,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 150000.0, "maintenanceMarginRate": 0.01, "maxLeverage": 25.0, "info": { "bracket": "3", "initialLeverage": "25", - "notionalCap": "100000", + "notionalCap": "150000", "notionalFloor": "25000", "maintMarginRatio": "0.01", "cum": "67.5" @@ -605,87 +605,87 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 300000.0, + "minNotional": 150000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "4", "initialLeverage": "20", - "notionalCap": "300000", - "notionalFloor": "100000", + "notionalCap": "600000", + "notionalFloor": "150000", "maintMarginRatio": "0.025", - "cum": "1567.5" + "cum": "2317.5" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 300000.0, - "maxNotional": 750000.0, + "minNotional": 600000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "5", "initialLeverage": "10", - "notionalCap": "750000", - "notionalFloor": "300000", + "notionalCap": "1500000", + "notionalFloor": "600000", "maintMarginRatio": "0.05", - "cum": "9067.5" + "cum": "17317.5" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 750000.0, - "maxNotional": 1500000.0, + "minNotional": 1500000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "1500000", - "notionalFloor": "750000", + "notionalCap": "3000000", + "notionalFloor": "1500000", "maintMarginRatio": "0.1", - "cum": "46567.5" + "cum": "92317.5" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 1500000.0, - "maxNotional": 3000000.0, + "minNotional": 3000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "7", "initialLeverage": "4", - "notionalCap": "3000000", - "notionalFloor": "1500000", + "notionalCap": "5000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.125", - "cum": "84067.5" + "cum": "167317.5" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 3000000.0, - "maxNotional": 6000000.0, + "minNotional": 5000000.0, + "maxNotional": 9000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "8", "initialLeverage": "2", - "notionalCap": "6000000", - "notionalFloor": "3000000", + "notionalCap": "9000000", + "notionalFloor": "5000000", "maintMarginRatio": "0.25", - "cum": "459067.5" + "cum": "792317.5" } }, { "tier": 9.0, "currency": "USDT", - "minNotional": 6000000.0, + "minNotional": 9000000.0, "maxNotional": 30000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, @@ -693,9 +693,9 @@ "bracket": "9", "initialLeverage": "1", "notionalCap": "30000000", - "notionalFloor": "6000000", + "notionalFloor": "9000000", "maintMarginRatio": "0.5", - "cum": "1959067.5" + "cum": "3042317.5" } } ], @@ -1114,10 +1114,10 @@ "minNotional": 0.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxLeverage": 10.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "10", "notionalCap": "100000", "notionalFloor": "0", "maintMarginRatio": "0.025", @@ -1130,10 +1130,10 @@ "minNotional": 100000.0, "maxNotional": 500000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxLeverage": 8.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "8", "notionalCap": "500000", "notionalFloor": "100000", "maintMarginRatio": "0.05", @@ -1192,13 +1192,13 @@ "tier": 6.0, "currency": "BUSD", "minNotional": 5000000.0, - "maxNotional": 8000000.0, + "maxNotional": 5500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "8000000", + "notionalCap": "5500000", "notionalFloor": "5000000", "maintMarginRatio": "0.5", "cum": "1527500.0" @@ -2759,14 +2759,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.02", + "maintMarginRatio": "0.025", "cum": "0.0" } }, @@ -2775,37 +2775,37 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "10", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" + "maintMarginRatio": "0.05", + "cum": "125.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 600000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.06, + "maxLeverage": 8.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "600000", + "initialLeverage": "8", + "notionalCap": "500000", "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" + "maintMarginRatio": "0.06", + "cum": "375.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 600000.0, + "minNotional": 500000.0, "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, @@ -2813,9 +2813,9 @@ "bracket": "4", "initialLeverage": "5", "notionalCap": "1600000", - "notionalFloor": "600000", + "notionalFloor": "500000", "maintMarginRatio": "0.1", - "cum": "30650.0" + "cum": "20375.0" } }, { @@ -2831,39 +2831,39 @@ "notionalCap": "2000000", "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "70650.0" + "cum": "60375.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 2000000.0, - "maxNotional": 6000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "6", "initialLeverage": "2", - "notionalCap": "6000000", + "notionalCap": "3000000", "notionalFloor": "2000000", "maintMarginRatio": "0.25", - "cum": "320650.0" + "cum": "310375.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 6000000.0, - "maxNotional": 10000000.0, + "minNotional": 3000000.0, + "maxNotional": 4000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "10000000", - "notionalFloor": "6000000", + "notionalCap": "4000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "1820650.0" + "cum": "1060375.0" } } ], @@ -5413,14 +5413,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 8.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "8", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.02", + "maintMarginRatio": "0.025", "cum": "0.0" } }, @@ -5428,80 +5428,64 @@ "tier": 2.0, "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, "info": { "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", + "initialLeverage": "6", + "notionalCap": "100000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" + "maintMarginRatio": "0.05", + "cum": "125.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", "minNotional": 100000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "4", + "bracket": "3", "initialLeverage": "5", "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "5125.0" } }, { - "tier": 5.0, + "tier": 4.0, "currency": "USDT", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 2.0, "info": { - "bracket": "5", + "bracket": "4", "initialLeverage": "2", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "11375.0" } }, { - "tier": 6.0, + "tier": 5.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "maxNotional": 1200000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "5", "initialLeverage": "1", - "notionalCap": "2000000", + "notionalCap": "1200000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "386375.0" } } ], @@ -5765,6 +5749,120 @@ } } ], + "BNT/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "BNX/USDT:USDT": [ { "tier": 1.0, @@ -7626,13 +7724,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 50000.0, - "maxNotional": 200000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "200000", + "notionalCap": "400000", "notionalFloor": "50000", "maintMarginRatio": "0.05", "cum": "1325.0" @@ -7641,49 +7739,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, + "minNotional": 400000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "200000", + "notionalCap": "1000000", + "notionalFloor": "400000", "maintMarginRatio": "0.1", - "cum": "11325.0" + "cum": "21325.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 1000000.0, + "maxNotional": 1200000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", + "initialLeverage": "4", + "notionalCap": "1200000", + "notionalFloor": "1000000", "maintMarginRatio": "0.125", - "cum": "23825.0" + "cum": "46325.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, + "minNotional": 1200000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "2000000", + "notionalFloor": "1200000", + "maintMarginRatio": "0.25", + "cum": "196325.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 2000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.5", - "cum": "398825.0" + "cum": "696325.0" } } ], @@ -8900,6 +9014,88 @@ "tier": 1.0, "currency": "BUSD", "minNotional": 0.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 8.0, + "info": { + "bracket": "1", + "initialLeverage": "8", + "notionalCap": "25000", + "notionalFloor": "0", + "maintMarginRatio": "0.025", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "2", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "625.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "3", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5625.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "4", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11875.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 1000000.0, + "maxNotional": 1200000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "5", + "initialLeverage": "1", + "notionalCap": "1200000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386875.0" + } + } + ], + "DODOX/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, @@ -8914,14 +9110,14 @@ }, { "tier": 2.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 15.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "15", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -8930,15 +9126,15 @@ }, { "tier": 3.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -8946,50 +9142,66 @@ }, { "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "10650.0" } }, { "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, + "currency": "USDT", + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "23150.0" } }, { "tier": 6.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "5000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "898150.0" } } ], @@ -16652,96 +16864,80 @@ "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, "info": { "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", + "initialLeverage": "10", + "notionalCap": "25000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.025", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 8.0, "info": { - "bracket": "3", + "bracket": "2", "initialLeverage": "8", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "625.0" } }, { - "tier": 4.0, + "tier": 3.0, "currency": "USDT", "minNotional": 100000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "4", + "bracket": "3", "initialLeverage": "5", "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "5625.0" } }, { - "tier": 5.0, + "tier": 4.0, "currency": "USDT", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 2.0, "info": { - "bracket": "5", + "bracket": "4", "initialLeverage": "2", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "11875.0" } }, { - "tier": 6.0, + "tier": 5.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "5", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "386875.0" } } ], @@ -17712,10 +17908,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 25.0, + "maxLeverage": 10.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "10", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -17728,10 +17924,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 15.0, + "maxLeverage": 8.0, "info": { "bracket": "2", - "initialLeverage": "15", + "initialLeverage": "8", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -17744,10 +17940,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "10", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -17806,13 +18002,13 @@ "tier": 7.0, "currency": "BUSD", "minNotional": 3000000.0, - "maxNotional": 8000000.0, + "maxNotional": 3500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "8000000", + "notionalCap": "3500000", "notionalFloor": "3000000", "maintMarginRatio": "0.5", "cum": "949400.0" @@ -19757,6 +19953,120 @@ } } ], + "OXT/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "PENDLE/USDT:USDT": [ { "tier": 1.0, @@ -24442,10 +24752,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 25.0, + "maxLeverage": 10.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "10", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -24458,10 +24768,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 15.0, + "maxLeverage": 8.0, "info": { "bracket": "2", - "initialLeverage": "15", + "initialLeverage": "8", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -24474,10 +24784,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "10", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -24536,13 +24846,13 @@ "tier": 7.0, "currency": "BUSD", "minNotional": 3000000.0, - "maxNotional": 8000000.0, + "maxNotional": 4000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "8000000", + "notionalCap": "4000000", "notionalFloor": "3000000", "maintMarginRatio": "0.5", "cum": "949400.0" @@ -25613,14 +25923,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.02", + "maintMarginRatio": "0.01", "cum": "0.0" } }, @@ -25629,95 +25939,111 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 15.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "15", + "initialLeverage": "25", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" + "maintMarginRatio": "0.02", + "cum": "50.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "200000", + "initialLeverage": "20", + "notionalCap": "50000", "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" + "maintMarginRatio": "0.025", + "cum": "175.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 50000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "200000", - "maintMarginRatio": "0.1", - "cum": "10650.0" + "initialLeverage": "10", + "notionalCap": "400000", + "notionalFloor": "50000", + "maintMarginRatio": "0.05", + "cum": "1425.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 500000.0, + "minNotional": 400000.0, "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "4", + "initialLeverage": "5", "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.125", - "cum": "23150.0" + "notionalFloor": "400000", + "maintMarginRatio": "0.1", + "cum": "21425.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "3000000", + "initialLeverage": "4", + "notionalCap": "2000000", "notionalFloor": "1000000", - "maintMarginRatio": "0.25", - "cum": "148150.0" + "maintMarginRatio": "0.125", + "cum": "46425.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 3000000.0, + "minNotional": 2000000.0, "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "5000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "296425.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "3000000", + "notionalCap": "10000000", + "notionalFloor": "5000000", "maintMarginRatio": "0.5", - "cum": "898150.0" + "cum": "1546425.0" } } ], @@ -26861,6 +27187,120 @@ } } ], + "YGG/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "ZEC/USDT:USDT": [ { "tier": 1.0, From 7651f1b1db8bbf49e7ea4fb46b170ec691db326f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Aug 2023 10:55:48 +0200 Subject: [PATCH 106/116] Add more samples for correct debug configurations --- docs/assets/pycharm_debug.png | Bin 0 -> 48984 bytes docs/developer.md | 16 +++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 docs/assets/pycharm_debug.png diff --git a/docs/assets/pycharm_debug.png b/docs/assets/pycharm_debug.png new file mode 100644 index 0000000000000000000000000000000000000000..b1c19ca93cdd42fb0f3e0740d62dc3266c2a6118 GIT binary patch literal 48984 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 ZF@9+UjK(n%6rvzsQsVMr Date: Wed, 16 Aug 2023 11:00:28 +0200 Subject: [PATCH 107/116] Remove wrong image link --- docs/developer.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 9c2052e4f..23f4a9b26 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -111,10 +111,9 @@ A similar setup can also be taken for Pycharm - using `freqtrade` as module name If your environment has not been detected, you can also pick a path manually. #### Pycharm - + In pycharm, you can select the appropriate Environment in the "Run/Debug Configurations" window. ![Pycharm debug configuration](assets/pycharm_debug.png) - ![Alt text](image.png) !!! Note "Startup directory" This assumes that you have the repository checked out, and the editor is started at the repository root level (so setup.py is at the top level of your repository). From 91fd472717086c1469750a182587313b85ad6edf Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Aug 2023 13:16:23 +0200 Subject: [PATCH 108/116] Extend ruff excludes to .venv --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 17f91c7b2..40c0e2005 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ ignore = ["freqtrade/vendor/**"] [tool.ruff] line-length = 100 -extend-exclude = [".env"] +extend-exclude = [".env", ".venv"] target-version = "py38" extend-select = [ "C90", # mccabe From d439936014a2a8ea9102fa56654293cdf2897b69 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Aug 2023 18:16:54 +0200 Subject: [PATCH 109/116] Update docs about suing `.venv` instead of `.env` --- docs/bot-usage.md | 2 +- docs/data-analysis.md | 2 +- docs/faq.md | 2 +- docs/hyperopt.md | 2 +- docs/installation.md | 18 +++++++++--------- docs/windows_installation.md | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index c6a7f6103..7aeda0c42 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -3,7 +3,7 @@ This page explains the different parameters of the bot and how to run it. !!! Note - If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands. + If you've used `setup.sh`, don't forget to activate your virtual environment (`source .venv/bin/activate`) before running freqtrade commands. !!! Warning "Up-to-date clock" The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. diff --git a/docs/data-analysis.md b/docs/data-analysis.md index afee9049f..7f5637dd0 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -27,7 +27,7 @@ For this to work, first activate your virtual environment and run the following ``` bash # Activate virtual environment -source .env/bin/activate +source .venv/bin/activate pip install ipykernel ipython kernel install --user --name=freqtrade diff --git a/docs/faq.md b/docs/faq.md index 0a3b152cb..50aaa03a3 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -36,7 +36,7 @@ Running the bot with `freqtrade trade --config config.json` shows the output `fr This could be caused by the following reasons: * The virtual environment is not active. - * Run `source .env/bin/activate` to activate the virtual environment. + * Run `source .venv/bin/activate` to activate the virtual environment. * The installation did not complete successfully. * Please check the [Installation documentation](installation.md). diff --git a/docs/hyperopt.md b/docs/hyperopt.md index b0920f9c4..3fbbee7f6 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -31,7 +31,7 @@ The docker-image includes hyperopt dependencies, no further action needed. ### Easy installation script (setup.sh) / Manual installation ```bash -source .env/bin/activate +source .venv/bin/activate pip install -r requirements-hyperopt.txt ``` diff --git a/docs/installation.md b/docs/installation.md index a06968dba..a89c83d28 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -143,11 +143,11 @@ If you are on Debian, Ubuntu or MacOS, freqtrade provides the script to install ### Activate your virtual environment -Each time you open a new terminal, you must run `source .env/bin/activate` to activate your virtual environment. +Each time you open a new terminal, you must run `source .venv/bin/activate` to activate your virtual environment. ```bash -# then activate your .env -source ./.env/bin/activate +# activate virtual environment +source ./.venv/bin/activate ``` ### Congratulations @@ -172,7 +172,7 @@ With this option, the script will install the bot and most dependencies: You will need to have git and python3.8+ installed beforehand for this to work. * Mandatory software as: `ta-lib` -* Setup your virtualenv under `.env/` +* Setup your virtualenv under `.venv/` This option is a combination of installation tasks and `--reset` @@ -225,11 +225,11 @@ rm -rf ./ta-lib* You will run freqtrade in separated `virtual environment` ```bash -# create virtualenv in directory /freqtrade/.env -python3 -m venv .env +# create virtualenv in directory /freqtrade/.venv +python3 -m venv .venv # run virtualenv -source .env/bin/activate +source .venv/bin/activate ``` #### Install python dependencies @@ -411,8 +411,8 @@ If you used (1)`Script` or (2)`Manual` installation, you need to run the bot in # if: bash: freqtrade: command not found -# then activate your .env -source ./.env/bin/activate +# then activate your virtual environment +source ./.venv/bin/activate ``` ### MacOS installation error diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 0327f21e5..db785a1fc 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -31,8 +31,8 @@ Other versions must be downloaded from the above link. ``` powershell cd \path\freqtrade -python -m venv .env -.env\Scripts\activate.ps1 +python -m venv .venv +.venv\Scripts\activate.ps1 # optionally install ta-lib from wheel # Eventually adjust the below filename to match the downloaded wheel pip install --find-links build_helpers\ TA-Lib -U From f607b8abfbdc39b8936e935606e4ced28368bd14 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Aug 2023 18:22:52 +0200 Subject: [PATCH 110/116] Silence pip download --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index 84f804021..5f116c0e9 100755 --- a/setup.sh +++ b/setup.sh @@ -11,7 +11,7 @@ function check_installed_pip() { ${PYTHON} -m pip > /dev/null if [ $? -ne 0 ]; then echo_block "Installing Pip for ${PYTHON}" - curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py + curl https://bootstrap.pypa.io/get-pip.py -s -o get-pip.py ${PYTHON} get-pip.py rm get-pip.py fi From ea181645b0040f854112706ae09d76e47333f02f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Aug 2023 18:33:19 +0200 Subject: [PATCH 111/116] setup.sh: Extract environment recration to function --- setup.sh | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/setup.sh b/setup.sh index 5f116c0e9..dd2588548 100755 --- a/setup.sh +++ b/setup.sh @@ -199,6 +199,23 @@ function check_git_changes() { fi } +function recreate_environments() { + if [ -d ".env" ]; then + # Remove old virtual env + echo "- Deleting your previous virtual env" + echo "Warning: Your new environment will be at .venv!" + rm -rf .env + fi + + echo + ${PYTHON} -m venv .venv + if [ $? -ne 0 ]; then + echo "Could not create virtual environment. Leaving now" + exit 1 + fi + +} + # Reset Develop or Stable branch function reset() { echo_block "Resetting branch and virtual env" @@ -225,17 +242,8 @@ function reset() { else echo "Reset ignored because you are not on 'stable' or 'develop'." fi + recreate_environments - if [ -d ".env" ]; then - echo "- Deleting your previous virtual env" - rm -rf .env - fi - echo - ${PYTHON} -m venv .env - if [ $? -ne 0 ]; then - echo "Could not create virtual environment. Leaving now" - exit 1 - fi updateenv } From 688018d9f291d6697f7537742a3142667d7f3691 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Aug 2023 18:51:24 +0200 Subject: [PATCH 112/116] Update setup.sh script to use `.venv` instead of `.env` --- setup.sh | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/setup.sh b/setup.sh index dd2588548..a53be205b 100755 --- a/setup.sh +++ b/setup.sh @@ -41,12 +41,12 @@ function check_installed_python() { } function updateenv() { - echo_block "Updating your virtual env" - if [ ! -f .env/bin/activate ]; then + echo_block "Updating your virtual environment" + if [ ! -f .venv/bin/activate ]; then echo "Something went wrong, no virtual environment found." exit 1 fi - source .env/bin/activate + source .venv/bin/activate SYS_ARCH=$(uname -m) echo "pip install in-progress. Please wait..." ${PYTHON} -m pip install --upgrade pip wheel setuptools @@ -186,7 +186,14 @@ function install_redhat() { # Upgrade the bot function update() { git pull + if [ -f .env/bin/activate ]; then + # Old environment found - updating to new environment. + recreate_environments + fi updateenv + echo "Update completed." + echo_block "Don't forget to activate your virtual enviorment with 'source .venv/bin/activate'!" + } function check_git_changes() { @@ -206,6 +213,10 @@ function recreate_environments() { echo "Warning: Your new environment will be at .venv!" rm -rf .env fi + if [ -d ".venv" ]; then + echo "- Deleting your previous virtual env" + rm -rf .venv + fi echo ${PYTHON} -m venv .venv @@ -274,9 +285,9 @@ function install() { reset config echo_block "Run the bot !" - echo "You can now use the bot by executing 'source .env/bin/activate; freqtrade '." - echo "You can see the list of available bot sub-commands by executing 'source .env/bin/activate; freqtrade --help'." - echo "You verify that freqtrade is installed successfully by running 'source .env/bin/activate; freqtrade --version'." + echo "You can now use the bot by executing 'source .venv/bin/activate; freqtrade '." + echo "You can see the list of available bot sub-commands by executing 'source .venv/bin/activate; freqtrade --help'." + echo "You verify that freqtrade is installed successfully by running 'source .venv/bin/activate; freqtrade --version'." } function plot() { From aa756221f63ee07ab7810aec9bd7e18ac23174f0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Aug 2023 18:56:42 +0200 Subject: [PATCH 113/116] Improve behavior of ta-lib install --- build_helpers/install_ta-lib.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build_helpers/install_ta-lib.sh b/build_helpers/install_ta-lib.sh index 005d9abca..0315c7025 100755 --- a/build_helpers/install_ta-lib.sh +++ b/build_helpers/install_ta-lib.sh @@ -8,8 +8,9 @@ if [ -n "$2" ] || [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then tar zxvf ta-lib-0.4.0-src.tar.gz cd ta-lib \ && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \ - && curl 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.guess' -o config.guess \ - && curl 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.sub' -o config.sub \ + && echo "Downloading gcc config.guess and config.sub" \ + && curl -s 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.guess' -o config.guess \ + && curl -s 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.sub' -o config.sub \ && ./configure --prefix=${INSTALL_LOC}/ \ && make if [ $? -ne 0 ]; then From b93c6235c112061597a82d433e5f7df637a3b02d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Aug 2023 18:59:22 +0200 Subject: [PATCH 114/116] Fix Case of gym.Env in documentation --- docs/freqai-reinforcement-learning.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/freqai-reinforcement-learning.md b/docs/freqai-reinforcement-learning.md index 1c95409ae..df4508c86 100644 --- a/docs/freqai-reinforcement-learning.md +++ b/docs/freqai-reinforcement-learning.md @@ -20,7 +20,7 @@ With the current framework, we aim to expose the training environment via the co We envision the majority of users focusing their effort on creative design of the `calculate_reward()` function [details here](#creating-a-custom-reward-function), while leaving the rest of the environment untouched. Other users may not touch the environment at all, and they will only play with the configuration settings and the powerful feature engineering that already exists in FreqAI. Meanwhile, we enable advanced users to create their own model classes entirely. -The framework is built on stable_baselines3 (torch) and OpenAI gym for the base environment class. But generally speaking, the model class is well isolated. Thus, the addition of competing libraries can be easily integrated into the existing framework. For the environment, it is inheriting from `gym.env` which means that it is necessary to write an entirely new environment in order to switch to a different library. +The framework is built on stable_baselines3 (torch) and OpenAI gym for the base environment class. But generally speaking, the model class is well isolated. Thus, the addition of competing libraries can be easily integrated into the existing framework. For the environment, it is inheriting from `gym.Env` which means that it is necessary to write an entirely new environment in order to switch to a different library. ### Important considerations @@ -173,7 +173,7 @@ class MyCoolRLModel(ReinforcementLearner): """ class MyRLEnv(Base5ActionRLEnv): """ - User made custom environment. This class inherits from BaseEnvironment and gym.env. + User made custom environment. This class inherits from BaseEnvironment and gym.Env. Users can override any functions from those parent classes. Here is an example of a user customized `calculate_reward()` function. @@ -254,7 +254,7 @@ FreqAI also provides a built in episodic summary logger called `self.tensorboard ```python class MyRLEnv(Base5ActionRLEnv): """ - User made custom environment. This class inherits from BaseEnvironment and gym.env. + User made custom environment. This class inherits from BaseEnvironment and gym.Env. Users can override any functions from those parent classes. Here is an example of a user customized `calculate_reward()` function. """ From 3bc49330cef6f4097438e073f04c11add1f5a903 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Aug 2023 09:15:59 +0200 Subject: [PATCH 115/116] webserver mode should properly validate config --- freqtrade/commands/webserver_commands.py | 5 +++-- freqtrade/configuration/config_validation.py | 2 ++ freqtrade/constants.py | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/webserver_commands.py b/freqtrade/commands/webserver_commands.py index 9a5975227..2fd7fe75c 100644 --- a/freqtrade/commands/webserver_commands.py +++ b/freqtrade/commands/webserver_commands.py @@ -7,9 +7,10 @@ def start_webserver(args: Dict[str, Any]) -> None: """ Main entry point for webserver mode """ - from freqtrade.configuration import Configuration + from freqtrade.configuration import setup_utils_configuration from freqtrade.rpc.api_server import ApiServer # Initialize configuration - config = Configuration(args, RunMode.WEBSERVER).get_config() + + config = setup_utils_configuration(args, RunMode.WEBSERVER) ApiServer(config, standalone=True) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index f1745df61..395826557 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -51,6 +51,8 @@ def validate_config_schema(conf: Dict[str, Any], preliminary: bool = False) -> D conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED else: conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED_FINAL + elif conf.get('runmode', RunMode.OTHER) == RunMode.WEBSERVER: + conf_schema['required'] = constants.SCHEMA_MINIMAL_WEBSERVER else: conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED try: diff --git a/freqtrade/constants.py b/freqtrade/constants.py index fedd34726..9a8d17909 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -667,6 +667,9 @@ SCHEMA_MINIMAL_REQUIRED = [ 'dataformat_ohlcv', 'dataformat_trades', ] +SCHEMA_MINIMAL_WEBSERVER = SCHEMA_MINIMAL_REQUIRED + [ + 'api_server', +] CANCEL_REASON = { "TIMEOUT": "cancelled due to timeout", From ba34318f7a47a96945947852f8649430ec691be9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Aug 2023 09:57:26 +0200 Subject: [PATCH 116/116] Update converter test to use fixture --- tests/data/test_converter.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 6a2cb5638..f23a85501 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -34,26 +34,21 @@ def test_ohlcv_to_dataframe(ohlcv_history_list, caplog): assert log_has('Converting candle (OHLCV) data to dataframe for pair UNITTEST/BTC.', caplog) -def test_trades_to_ohlcv(ohlcv_history_list, caplog): +def test_trades_to_ohlcv(trades_history, caplog): caplog.set_level(logging.DEBUG) with pytest.raises(ValueError, match="Trade-list empty."): trades_to_ohlcv([], '1m') - trades = [ - [1570752011620, "13519807", None, "sell", 0.00141342, 23.0, 0.03250866], - [1570752011620, "13519808", None, "sell", 0.00141266, 54.0, 0.07628364], - [1570752017964, "13519809", None, "sell", 0.00141266, 8.0, 0.01130128]] - - df = trades_to_ohlcv(trades, '1m') + df = trades_to_ohlcv(trades_history, '1m') assert not df.empty assert len(df) == 1 assert 'open' in df.columns assert 'high' in df.columns assert 'low' in df.columns assert 'close' in df.columns - assert df.loc[:, 'high'][0] == 0.00141342 - assert df.loc[:, 'low'][0] == 0.00141266 + assert df.loc[:, 'high'][0] == 0.019627 + assert df.loc[:, 'low'][0] == 0.019626 def test_ohlcv_fill_up_missing_data(testdatadir, caplog):