| reversal_confirmation.lookback_period | 0 | int >= 0 | Prior confirming candles; 0 = none. |
| reversal_confirmation.decay_ratio | 0.5 | float (0,1] | Geometric per-candle relaxation factor. |
| reversal_confirmation.min_natr_ratio_percent | 0.0099 | float [0,1] | Lower bound fraction for volatility adjusted reversal threshold. |
-| reversal_confirmation.max_natr_ratio_percent | 0.035 | float [0,1] | Upper bound fraction (>= lower bound) for volatility adjusted reversal threshold. |
+| reversal_confirmation.max_natr_ratio_percent | 0.4 | float [0,1] | Upper bound fraction (>= lower bound) for volatility adjusted reversal threshold. |
| _Regressor model_ | | | |
| freqai.regressor | `xgboost` | enum {`xgboost`,`lightgbm`} | Machine learning regressor algorithm. |
| _Extrema smoothing_ | | | |
| freqai.extrema_smoothing_window | 5 | int >= 1 | Window size for extrema smoothing. |
| freqai.extrema_smoothing_beta | 8.0 | float > 0 | Kaiser kernel shape parameter. |
| _Feature parameters_ | | | |
-| freqai.feature_parameters.label_period_candles | 24 | int >= 1 | Zigzag NATR horizon. |
-| freqai.feature_parameters.label_natr_ratio | 9.0 | float > 0 | Zigzag NATR ratio. |
-| freqai.feature_parameters.min_label_natr_ratio | 9.0 | float > 0 | Minimum NATR ratio bound used by label HPO. |
-| freqai.feature_parameters.max_label_natr_ratio | 12.0 | float > 0 | Maximum NATR ratio bound used by label HPO. |
+| freqai.feature_parameters.label_period_candles | min/max midpoint | int >= 1 | Zigzag labeling NATR horizon. |
+| freqai.feature_parameters.min_label_period_candles | 12 | int >= 1 | Minimum labeling NATR horizon used for reversals labeling HPO. |
+| freqai.feature_parameters.max_label_period_candles | 24 | int >= 1 | Maximum labeling NATR horizon used for reversals labeling HPO. |
+| freqai.feature_parameters.label_natr_ratio | min/max midpoint | float > 0 | Zigzag labeling NATR ratio. |
+| freqai.feature_parameters.min_label_natr_ratio | 9.0 | float > 0 | Minimum labeling NATR ratio used for reversals labeling HPO. |
+| freqai.feature_parameters.max_label_natr_ratio | 12.0 | float > 0 | Maximum labeling NATR ratio used for reversals labeling HPO. |
| freqai.feature_parameters.label_frequency_candles | `auto` | int >= 2 \| `auto` | Reversals labeling frequency. `auto` = max(2, 2 * number of whitelisted pairs). |
| freqai.feature_parameters.label_metric | `euclidean` | string (supported: `euclidean`,`minkowski`,`cityblock`,`chebyshev`,`mahalanobis`,`seuclidean`,`jensenshannon`,`sqeuclidean`,...) | Metric used in distance calculations to ideal point. |
| freqai.feature_parameters.label_weights | [0.5,0.5] | list[float] | Per-objective weights used in distance calculations to ideal point. |
"DI_cutoff": 2,
"&s-minima_threshold": -2,
"&s-maxima_threshold": 2,
- "label_period_candles": 24,
- "label_natr_ratio": 9.0,
+ "label_period_candles": 18,
+ "label_natr_ratio": 10.5,
"hp_rmse": -1,
"train_rmse": -1
},
"4h",
// "1d"
],
- "label_period_candles": 24,
+ "label_period_candles": 18,
"label_metric": "euclidean",
"label_weights": [
0.5,
get_min_max_label_period_candles,
get_optuna_callbacks,
get_optuna_study_model_parameters,
+ midpoint,
soft_extremum,
+ validate_range,
zigzag,
)
https://github.com/sponsors/robcaulk
"""
- version = "3.7.116"
+ version = "3.7.117"
@cached_property
def _optuna_config(self) -> dict[str, Any]:
self._optuna_label_candle: dict[str, int] = {}
self._optuna_label_candles: dict[str, int] = {}
self._optuna_label_incremented_pairs: list[str] = []
+ self._init_label_defaults()
for pair in self.pairs:
self._optuna_hp_value[pair] = -1
self._optuna_train_value[pair] = -1
if self.optuna_load_best_params(pair, "label")
else {
"label_period_candles": self.ft_params.get(
- "label_period_candles", 24
+ "label_period_candles",
+ self._default_label_period_candles,
),
"label_natr_ratio": float(
- self.ft_params.get("label_natr_ratio", 9.0)
+ self.ft_params.get(
+ "label_natr_ratio",
+ self._default_label_natr_ratio,
+ )
),
}
)
f"Initialized {self.__class__.__name__} {self.freqai_info.get('regressor', 'xgboost')} regressor model version {self.version}"
)
+ def _init_label_defaults(self) -> None:
+ default_min_label_natr_ratio = 9.0
+ default_max_label_natr_ratio = 12.0
+ min_label_natr_ratio = self.ft_params.get(
+ "min_label_natr_ratio", default_min_label_natr_ratio
+ )
+ max_label_natr_ratio = self.ft_params.get(
+ "max_label_natr_ratio", default_max_label_natr_ratio
+ )
+ min_label_natr_ratio, max_label_natr_ratio = validate_range(
+ min_label_natr_ratio,
+ max_label_natr_ratio,
+ logger,
+ name="label_natr_ratio",
+ default_min=default_min_label_natr_ratio,
+ default_max=default_max_label_natr_ratio,
+ allow_equal=False,
+ non_negative=True,
+ finite_only=True,
+ )
+ self._default_label_natr_ratio = float(
+ midpoint(min_label_natr_ratio, max_label_natr_ratio)
+ )
+
+ default_min_label_period_candles = 12
+ default_max_label_period_candles = 24
+ min_label_period_candles = self.ft_params.get(
+ "min_label_period_candles", default_min_label_period_candles
+ )
+ max_label_period_candles = self.ft_params.get(
+ "max_label_period_candles", default_max_label_period_candles
+ )
+ min_label_period_candles, max_label_period_candles = validate_range(
+ min_label_period_candles,
+ max_label_period_candles,
+ logger,
+ name="label_period_candles",
+ default_min=default_min_label_period_candles,
+ default_max=default_max_label_period_candles,
+ allow_equal=True,
+ non_negative=True,
+ finite_only=True,
+ )
+ self._default_label_period_candles = int(
+ round(midpoint(min_label_period_candles, max_label_period_candles))
+ )
+
def get_optuna_params(self, pair: str, namespace: str) -> dict[str, Any]:
if namespace == "hp":
params = self._optuna_hp_params.get(pair)
),
fit_live_predictions_candles,
self._optuna_config.get("label_candles_step"),
+ min_label_period_candles=self.ft_params.get(
+ "min_label_period_candles", 12
+ ),
+ max_label_period_candles=self.ft_params.get(
+ "max_label_period_candles", 24
+ ),
min_label_natr_ratio=self.ft_params.get(
"min_label_natr_ratio", 9.0
),
df: pd.DataFrame,
fit_live_predictions_candles: int,
candles_step: int,
+ min_label_period_candles: int = 12,
+ max_label_period_candles: int = 24,
min_label_natr_ratio: float = 9.0,
max_label_natr_ratio: float = 12.0,
) -> tuple[float, int]:
min_label_period_candles, max_label_period_candles, candles_step = (
- get_min_max_label_period_candles(fit_live_predictions_candles, candles_step)
+ get_min_max_label_period_candles(
+ fit_live_predictions_candles,
+ candles_step,
+ min_label_period_candles=min_label_period_candles,
+ max_label_period_candles=max_label_period_candles,
+ min_label_period_candles_fallback=min_label_period_candles,
+ max_label_period_candles_fallback=max_label_period_candles,
+ )
)
label_period_candles = trial.suggest_int(
get_callable_sha256,
get_distance,
get_zl_ma_fn,
+ midpoint,
non_zero_diff,
price_retracement_percent,
smooth_extrema,
top_change_percent,
+ validate_range,
vwapb,
zigzag,
zlema,
INTERFACE_VERSION = 3
def version(self) -> str:
- return "3.3.160"
+ return "3.3.163"
timeframe = "5m"
"lookback_period": 0,
"decay_ratio": 0.5,
"min_natr_ratio_percent": 0.0099,
- "max_natr_ratio_percent": 0.035,
+ "max_natr_ratio_percent": 0.4,
}
position_adjustment_enable = True
/ "models"
/ self.freqai_info.get("identifier")
)
+ self._init_label_defaults()
self._label_params: dict[str, dict[str, Any]] = {}
for pair in self.pairs:
self._label_params[pair] = (
self.optuna_load_best_params(pair, "label")
if self.optuna_load_best_params(pair, "label")
else {
- "label_period_candles": self.freqai_info["feature_parameters"].get(
- "label_period_candles", 24
+ "label_period_candles": self.freqai_info.get(
+ "feature_parameters", {}
+ ).get(
+ "label_period_candles",
+ self._default_label_period_candles,
),
"label_natr_ratio": float(
- self.freqai_info["feature_parameters"].get(
- "label_natr_ratio", 9.0
+ self.freqai_info.get("feature_parameters", {}).get(
+ "label_natr_ratio",
+ self._default_label_natr_ratio,
)
),
}
)
+ self._init_reversal_confirmation_defaults()
self._candle_duration_secs = int(
timeframe_to_minutes(self.config.get("timeframe")) * 60
)
**self.config.get("exit_pricing", {}).get("thresholds_calibration", {}),
}
+ def _init_reversal_confirmation_defaults(self) -> None:
+ reversal_confirmation = self.config.get("reversal_confirmation", {})
+ lookback_period = reversal_confirmation.get(
+ "lookback_period",
+ QuickAdapterV3.default_reversal_confirmation["lookback_period"],
+ )
+ decay_ratio = reversal_confirmation.get(
+ "decay_ratio", QuickAdapterV3.default_reversal_confirmation["decay_ratio"]
+ )
+ min_natr_ratio_percent = reversal_confirmation.get(
+ "min_natr_ratio_percent",
+ QuickAdapterV3.default_reversal_confirmation["min_natr_ratio_percent"],
+ )
+ max_natr_ratio_percent = reversal_confirmation.get(
+ "max_natr_ratio_percent",
+ QuickAdapterV3.default_reversal_confirmation["max_natr_ratio_percent"],
+ )
+
+ if not isinstance(lookback_period, int) or lookback_period < 0:
+ logger.warning(
+ f"reversal_confirmation: invalid lookback_period {lookback_period!r}, using default {QuickAdapterV3.default_reversal_confirmation['lookback_period']}"
+ )
+ lookback_period = QuickAdapterV3.default_reversal_confirmation[
+ "lookback_period"
+ ]
+
+ if not isinstance(decay_ratio, (int, float)) or not (
+ 0.0 < float(decay_ratio) <= 1.0
+ ):
+ logger.warning(
+ f"reversal_confirmation: invalid decay_ratio {decay_ratio!r}, using default {QuickAdapterV3.default_reversal_confirmation['decay_ratio']}"
+ )
+ decay_ratio = QuickAdapterV3.default_reversal_confirmation["decay_ratio"]
+ else:
+ decay_ratio = float(decay_ratio)
+
+ min_natr_ratio_percent, max_natr_ratio_percent = validate_range(
+ min_natr_ratio_percent,
+ max_natr_ratio_percent,
+ logger,
+ name="natr_ratio_percent",
+ default_min=QuickAdapterV3.default_reversal_confirmation[
+ "min_natr_ratio_percent"
+ ],
+ default_max=QuickAdapterV3.default_reversal_confirmation[
+ "max_natr_ratio_percent"
+ ],
+ allow_equal=False,
+ non_negative=True,
+ finite_only=True,
+ )
+
+ self._reversal_lookback_period = int(lookback_period)
+ self._reversal_decay_ratio = float(decay_ratio)
+ self._reversal_min_natr_ratio_percent = float(min_natr_ratio_percent)
+ self._reversal_max_natr_ratio_percent = float(max_natr_ratio_percent)
+
+ logger.debug(
+ "reversal_confirmation: lookback_period=%s, decay_ratio=%s, natr_ratio_percent_range=(%s, %s)",
+ self._reversal_lookback_period,
+ format_number(self._reversal_decay_ratio),
+ format_number(self._reversal_min_natr_ratio_percent),
+ format_number(self._reversal_max_natr_ratio_percent),
+ )
+
+ def _init_label_defaults(self) -> None:
+ feature_parameters = self.freqai_info.get("feature_parameters", {})
+
+ default_min_label_natr_ratio = 9.0
+ default_max_label_natr_ratio = 12.0
+ min_label_natr_ratio = feature_parameters.get(
+ "min_label_natr_ratio", default_min_label_natr_ratio
+ )
+ max_label_natr_ratio = feature_parameters.get(
+ "max_label_natr_ratio", default_max_label_natr_ratio
+ )
+ min_label_natr_ratio, max_label_natr_ratio = validate_range(
+ min_label_natr_ratio,
+ max_label_natr_ratio,
+ logger,
+ name="label_natr_ratio",
+ default_min=default_min_label_natr_ratio,
+ default_max=default_max_label_natr_ratio,
+ allow_equal=False,
+ non_negative=True,
+ finite_only=True,
+ )
+ self._default_label_natr_ratio = float(
+ midpoint(min_label_natr_ratio, max_label_natr_ratio)
+ )
+
+ default_min_label_period_candles = 12
+ default_max_label_period_candles = 24
+ min_label_period_candles = feature_parameters.get(
+ "min_label_period_candles", default_min_label_period_candles
+ )
+ max_label_period_candles = feature_parameters.get(
+ "max_label_period_candles", default_max_label_period_candles
+ )
+ min_label_period_candles, max_label_period_candles = validate_range(
+ min_label_period_candles,
+ max_label_period_candles,
+ logger,
+ name="label_period_candles",
+ default_min=default_min_label_period_candles,
+ default_max=default_max_label_period_candles,
+ allow_equal=True,
+ non_negative=True,
+ finite_only=True,
+ )
+ self._default_label_period_candles = int(
+ round(midpoint(min_label_period_candles, max_label_period_candles))
+ )
+
def feature_engineering_expand_all(
self, dataframe: DataFrame, period: int, metadata: dict[str, Any], **kwargs
) -> DataFrame:
)
if label_period_candles and isinstance(label_period_candles, int):
return label_period_candles
- return self.freqai_info["feature_parameters"].get("label_period_candles", 24)
+ return self.freqai_info.get("feature_parameters", {}).get(
+ "label_period_candles",
+ self._default_label_period_candles,
+ )
def set_label_period_candles(self, pair: str, label_period_candles: int) -> None:
if isinstance(label_period_candles, int):
if label_natr_ratio and isinstance(label_natr_ratio, float):
return label_natr_ratio
return float(
- self.freqai_info["feature_parameters"].get("label_natr_ratio", 9.0)
+ self.freqai_info.get("feature_parameters", {}).get(
+ "label_natr_ratio",
+ self._default_label_natr_ratio,
+ )
)
def set_label_natr_ratio(self, pair: str, label_natr_ratio: float) -> None:
return False
if order not in {"entry", "exit"}:
return False
+
trade_direction = side
- if (
- min_natr_ratio_percent < 0.0
- or max_natr_ratio_percent < min_natr_ratio_percent
- ):
- logger.warning(
- f"User denied {trade_direction} {order} for {pair}: invalid natr_ratio_percent range "
- f"min={format_number(min_natr_ratio_percent)}, max={format_number(max_natr_ratio_percent)}"
- )
- return False
- if not isinstance(lookback_period, int):
- logger.info(
- f"User denied {trade_direction} {order} for {pair}: invalid lookback_period type"
- )
- return False
- if lookback_period < 0:
- logger.info(
- f"User denied {trade_direction} {order} for {pair}: negative lookback_period={lookback_period}"
- )
- return False
max_lookback_period = max(0, len(df) - 1)
if lookback_period > max_lookback_period:
lookback_period = max_lookback_period
trade.set_custom_data("n_outliers", n_outliers)
trade.set_custom_data("last_outlier_date", last_candle_date.isoformat())
- lookback_period: int = self.config.get("reversal_confirmation", {}).get(
- "lookback_period",
- QuickAdapterV3.default_reversal_confirmation["lookback_period"],
- )
- decay_ratio: float = self.config.get("reversal_confirmation", {}).get(
- "decay_ratio", QuickAdapterV3.default_reversal_confirmation["decay_ratio"]
- )
- min_natr_ratio_percent: float = self.config.get(
- "reversal_confirmation", {}
- ).get(
- "min_natr_ratio_percent",
- QuickAdapterV3.default_reversal_confirmation["min_natr_ratio_percent"],
- )
- max_natr_ratio_percent: float = self.config.get(
- "reversal_confirmation", {}
- ).get(
- "max_natr_ratio_percent",
- QuickAdapterV3.default_reversal_confirmation["max_natr_ratio_percent"],
- )
if (
trade.trade_direction == "short"
and last_candle.get("do_predict") == 1
"long",
"exit",
current_rate,
- lookback_period,
- decay_ratio,
- min_natr_ratio_percent,
- max_natr_ratio_percent,
+ self._reversal_lookback_period,
+ self._reversal_decay_ratio,
+ self._reversal_min_natr_ratio_percent,
+ self._reversal_max_natr_ratio_percent,
)
):
return "minima_detected_short"
"short",
"exit",
current_rate,
- lookback_period,
- decay_ratio,
- min_natr_ratio_percent,
- max_natr_ratio_percent,
+ self._reversal_lookback_period,
+ self._reversal_decay_ratio,
+ self._reversal_min_natr_ratio_percent,
+ self._reversal_max_natr_ratio_percent,
)
):
return "maxima_detected_long"
side,
"entry",
rate,
- self.config.get("reversal_confirmation", {}).get(
- "lookback_period",
- QuickAdapterV3.default_reversal_confirmation["lookback_period"],
- ),
- self.config.get("reversal_confirmation", {}).get(
- "decay_ratio",
- QuickAdapterV3.default_reversal_confirmation["decay_ratio"],
- ),
- self.config.get("reversal_confirmation", {}).get(
- "min_natr_ratio_percent",
- QuickAdapterV3.default_reversal_confirmation["min_natr_ratio_percent"],
- ),
- self.config.get("reversal_confirmation", {}).get(
- "max_natr_ratio_percent",
- QuickAdapterV3.default_reversal_confirmation["max_natr_ratio_percent"],
- ),
+ self._reversal_lookback_period,
+ self._reversal_decay_ratio,
+ self._reversal_min_natr_ratio_percent,
+ self._reversal_max_natr_ratio_percent,
):
return True
return False
import math
from enum import IntEnum
from functools import lru_cache
-from statistics import median
+from logging import Logger
from typing import Any, Callable, Literal, Optional, TypeVar
import numpy as np
return abs(p1 - p2)
+def midpoint(value1: T, value2: T) -> T:
+ """Calculate the midpoint (geometric center) between two values."""
+ return (value1 + value2) / 2
+
+
def non_zero_diff(s1: pd.Series, s2: pd.Series) -> pd.Series:
"""Returns the difference of two series and replaces zeros with epsilon."""
diff = s1 - s2
) -> float:
volatility_quantile = calculate_volatility_quantile(pos)
if np.isnan(volatility_quantile):
- return median([min_threshold, max_threshold])
+ return midpoint(min_threshold, max_threshold)
return max_threshold - (max_threshold - min_threshold) * volatility_quantile
for param, (default_min, default_max) in default_ranges.items():
center_value = model_training_best_parameters.get(param)
- if (
- center_value is None
- or not isinstance(center_value, (int, float))
- or not np.isfinite(center_value)
+ if center_value is None:
+ center_value = midpoint(default_min, default_max)
+ elif not isinstance(center_value, (int, float)) or not np.isfinite(
+ center_value
):
continue
fit_live_predictions_candles: int,
candles_step: int,
min_label_period_candles: int = 12,
- max_label_period_candles: int = 36,
+ max_label_period_candles: int = 24,
max_period_candles: int = 36,
max_horizon_fraction: float = 1.0 / 3.0,
min_label_period_candles_fallback: int = 12,
- max_label_period_candles_fallback: int = 36,
+ max_label_period_candles_fallback: int = 24,
) -> tuple[int, int, int]:
if min_label_period_candles > max_label_period_candles:
raise ValueError(
if not np.isfinite(value):
raise ValueError("value must be finite")
return int(math.floor(float(value) / step) * step)
+
+
+def validate_range(
+ min_val: float | int,
+ max_val: float | int,
+ logger: Logger,
+ *,
+ name: str,
+ default_min: float | int,
+ default_max: float | int,
+ allow_equal: bool = False,
+ non_negative: bool = True,
+ finite_only: bool = True,
+) -> tuple[float | int, float | int]:
+ min_name = f"min_{name}"
+ max_name = f"max_{name}"
+
+ if not isinstance(default_min, (int, float)) or not isinstance(
+ default_max, (int, float)
+ ):
+ raise ValueError(f"{name}: defaults must be numeric")
+ if default_min > default_max or (not allow_equal and default_min == default_max):
+ raise ValueError(
+ f"{name}: invalid defaults ordering {default_min} >= {default_max}"
+ )
+
+ def _validate_component(
+ value: float | int | None, name: str, default_value: float | int
+ ) -> float | int:
+ ok = True
+ if not isinstance(value, (int, float)):
+ ok = False
+ elif isinstance(value, bool):
+ ok = False
+ elif finite_only and not np.isfinite(value):
+ ok = False
+ elif non_negative and value < 0:
+ ok = False
+ if not ok:
+ logger.warning(
+ f"{name}: invalid value {value!r}, using default {default_value}"
+ )
+ return default_value
+ return value
+
+ sanitized_min = _validate_component(min_val, min_name, default_min)
+ sanitized_max = _validate_component(max_val, max_name, default_max)
+
+ ordering_ok = (
+ (sanitized_min < sanitized_max)
+ if not allow_equal
+ else (sanitized_min <= sanitized_max)
+ )
+ if not ordering_ok:
+ logger.warning(
+ f"{name}: invalid ordering ({min_name}={sanitized_min}, {max_name}={sanitized_max}); using defaults ({default_min}, {default_max})"
+ )
+ sanitized_min, sanitized_max = default_min, default_max
+
+ if sanitized_min != min_val or sanitized_max != max_val:
+ logger.warning(
+ f"{name}: sanitized {min_name}={sanitized_min}, {max_name}={sanitized_max} (defaults=({default_min}, {default_max}))"
+ )
+
+ return sanitized_min, sanitized_max