### Configuration tunables
-| Path | Default | Type / Range | Description |
-| -------------------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| _Protections_ | | | |
-| custom_protections.trade_duration_candles | 72 | int >= 1 | Estimated trade duration in candles. Scales protections stop duration candles and trade limit. |
-| custom_protections.lookback_period_fraction | 0.5 | float (0,1] | Fraction of `fit_live_predictions_candles` used to calculate `lookback_period_candles` for _MaxDrawdown_ and _StoplossGuard_ protections. |
-| custom_protections.cooldown.enabled | true | bool | Enable/disable _CooldownPeriod_ protection. |
-| custom_protections.cooldown.stop_duration_candles | 4 | int >= 1 | Number of candles to wait before allowing new trades after a trade is closed. |
-| custom_protections.drawdown.enabled | true | bool | Enable/disable _MaxDrawdown_ protection. |
-| custom_protections.drawdown.max_allowed_drawdown | 0.2 | float (0,1) | Maximum allowed drawdown. |
-| custom_protections.stoploss.enabled | true | bool | Enable/disable _StoplossGuard_ protection. |
-| _Leverage_ | | | |
-| leverage | `proposed_leverage` | float [1.0, max_leverage] | Leverage. Fallback to `proposed_leverage` for the pair. |
-| _Exit pricing_ | | | |
-| exit_pricing.trade_price_target_method | `moving_average` | enum {`moving_average`,`quantile_interpolation`,`weighted_average`} | Trade NATR computation method. |
-| exit_pricing.thresholds_calibration.decline_quantile | 0.5 | float (0,1) | PnL decline quantile threshold. |
-| _Reversal confirmation_ | | | |
-| reversal_confirmation.lookback_period_candles | 0 | int >= 0 | Prior confirming candles; 0 = none. |
-| reversal_confirmation.decay_fraction | 0.5 | float (0,1] | Geometric per-candle volatility adjusted reversal threshold relaxation factor. |
-| reversal_confirmation.min_natr_multiplier_fraction | 0.0095 | float [0,1] | Lower bound fraction for volatility adjusted reversal threshold. |
-| reversal_confirmation.max_natr_multiplier_fraction | 0.0125 | float [0,1] | Upper bound fraction (>= lower bound) for volatility adjusted reversal threshold. |
-| _Regressor model_ | | | |
-| freqai.regressor | `xgboost` | enum {`xgboost`,`lightgbm`,`histgradientboostingregressor`,`ngboost`,`catboost`} | Machine learning regressor algorithm. |
-| _Model training parameters_ | | | |
-| freqai.model_training_parameters.gpu_vram_gb | 80 | enum {8,10,12,16,24,32,40,48,64,80} | Available GPU VRAM (GB) for CatBoost, not total. Constrains `depth`, `border_count`, and `max_ctr_complexity` ranges. |
-| _Data split parameters_ | | | |
-| freqai.data_split_parameters.method | `train_test_split` | enum {`train_test_split`,`timeseries_split`} | Data splitting strategy. `train_test_split` for sequential split, `timeseries_split` for chronological split with configurable gap. |
-| freqai.data_split_parameters.test_size | 0.1 / None | float (0,1) \| int >= 1 \| None | Test set size. Float for fraction, int for count. Default: 0.1 for `train_test_split`, None for `timeseries_split` (sklearn dynamic sizing). |
-| freqai.data_split_parameters.n_splits | 5 | int >= 2 | Controls train/test proportions for `timeseries_split` (higher = larger train set). |
-| freqai.data_split_parameters.gap | 0 | int >= 0 | Samples to exclude between train/test for `timeseries_split`. When `0` and `causal_mode=true` (default), auto-set from `label_horizon_candles`; when `0` and `causal_mode=false`, auto-set from `label_period_candles`. Under `causal_mode=true`, an explicit `gap<label_horizon_candles` is rejected. |
-| freqai.data_split_parameters.max_train_size | None | int >= 1 \| None | Maximum training set size for `timeseries_split`. When set, creates a sliding window instead of expanding train set. None = no limit. |
-| _Label smoothing_ | | | |
-| freqai.label_smoothing.method | `gaussian` | enum {`none`,`gaussian`,`kaiser`,`kaiser_bessel_derived`,`triang`,`smm`,`sma`,`savgol`,`gaussian_filter1d`} | Label smoothing method (`kaiser_bessel_derived` uses an even-length Kaiser-Bessel-derived zero-phase kernel; `smm`=median, `sma`=mean, `savgol`=Savitzky–Golay). |
-| freqai.label_smoothing.window_candles | 5 | int >= 3 | Smoothing window length (candles). |
-| freqai.label_smoothing.beta | 8.0 | float > 0 | Shape parameter for `kaiser` and `kaiser_bessel_derived` kernels. |
-| freqai.label_smoothing.polyorder | 3 | int >= 0 | Polynomial order for `savgol` smoothing. |
-| freqai.label_smoothing.mode | `mirror` | enum {`mirror`,`constant`,`nearest`,`wrap`,`interp`} | Boundary mode for `savgol` and `gaussian_filter1d`. |
-| freqai.label_smoothing.sigma | 1.0 | float > 0 | Gaussian `sigma` for `gaussian_filter1d` smoothing. |
-| _Label weighting_ | | | |
-| freqai.label_weighting.strategy | `none` | enum {`none`,`uniform`,`amplitude`,`amplitude_threshold_ratio`,`volume_rate`,`speed`,`efficiency_ratio`,`volume_weighted_efficiency_ratio`,`combined`} | Label weighting metric: none (`none`), uniform unit weight on every detected pivot (`uniform`), swing amplitude (`amplitude`), swing amplitude / median volatility-threshold ratio (`amplitude_threshold_ratio`), swing volume per candle (`volume_rate`), swing speed (`speed`), swing efficiency ratio (`efficiency_ratio`), swing volume-weighted efficiency ratio (`volume_weighted_efficiency_ratio`), or combined metrics aggregation (`combined`). Switching between `none` and any other strategy requires deleting trained models to realign training emphasis. |
-| freqai.label_weighting.metric_coefficients | {} | dict[str, float] | Per-metric coefficients for `combined` strategy. Keys: `amplitude`, `amplitude_threshold_ratio`, `volume_rate`, `speed`, `efficiency_ratio`, `volume_weighted_efficiency_ratio`. |
-| freqai.label_weighting.aggregation | `arithmetic_mean` | enum {`arithmetic_mean`,`geometric_mean`,`harmonic_mean`,`quadratic_mean`,`weighted_median`,`softmax`} | Metric aggregation method for `combined` strategy. `arithmetic_mean`=(Σ(w·m)/Σ(w)), `geometric_mean`=(∏(m^w))^(1/Σw), `harmonic_mean`=Σ(w)/(Σ(w/m)), `quadratic_mean`=(Σ(w·m²)/Σ(w))^(1/2), `weighted_median`=Q₀.₅(m,w), `softmax`=Σ(m·s_i) where s_i=w_i·exp(m_i/T)/Σ(w_j·exp(m_j/T)). |
-| freqai.label_weighting.softmax_temperature | 1.0 | float > 0 | Temperature T for `softmax` aggregation, controls distribution sharpness. |
-| freqai.label_weighting.fill_method | `zero` | enum {`zero`,`epsilon`,`gaussian`,`epsilon_gaussian`} | Off-pivot weighting scheme. `zero` hard-zeros off-pivot rows; `epsilon` applies the epsilon floor `fill_epsilon * <fill_epsilon_baseline>(pivot_weights)`; `gaussian` applies per-pivot Gaussian bumps; `epsilon_gaussian` sums the `epsilon` floor and the `gaussian` bumps. Pivot rows take the max of their raw weight and the off-pivot field at their index (no-op for `zero`). Switching away from `zero` may require retuning tree-leaf regularization (`min_child_weight`, `lambda`) and resetting any prior Optuna study. Changing this parameter requires deleting trained models. |
-| freqai.label_weighting.fill_epsilon | 0.000001 | float [0,1] | Off-pivot fraction of the pivot baseline. Ignored when `fill_method` not in {`epsilon`,`epsilon_gaussian`}. |
-| freqai.label_weighting.fill_epsilon_baseline | `mean` | enum {`mean`,`median`} | Pivot baseline statistic. `mean` tracks central tendency; `median` is robust against pivot-weight skew. Ignored when `fill_method` not in {`epsilon`,`epsilon_gaussian`}. |
-| freqai.label_weighting.fill_sigma_candles | 25.0 | float >= 0.5 | Gaussian standard deviation in candles for the per-pivot bumps. Acts as the upper bound on per-pivot sigma when `fill_bandwidth == "knn"`. Lower bound 0.5 prevents severe underflow in the Gaussian tail. Ignored when `fill_method` not in {`gaussian`,`epsilon_gaussian`}. |
-| freqai.label_weighting.fill_sigma_min_candles | 0.5 | float >= 0.5 | Lower bound on per-pivot sigma in candles when `fill_bandwidth == "knn"`. Clipped to `fill_sigma_candles` when larger. Ignored when `fill_method` not in {`gaussian`,`epsilon_gaussian`} or `fill_bandwidth != "knn"`. |
-| freqai.label_weighting.fill_bandwidth | `fixed` | enum {`fixed`,`knn`} | Per-pivot Gaussian bandwidth selector. `fixed` applies a constant `fill_sigma_candles` to every pivot (legacy behavior). `knn` adapts each pivot's sigma to local pivot density via `sigma_p = clip(fill_bandwidth_alpha * d_k(p), fill_sigma_min_candles, fill_sigma_candles)` where `d_k(p)` is the index distance to the `k`-th nearest pivot neighbor (Loftsgaarden & Quesenberry 1965; Silverman 1986, §5.2). Mitigates the crushing of weaker pivots by stronger neighbors in dense clusters. Ignored when `fill_method` not in {`gaussian`,`epsilon_gaussian`}. |
-| freqai.label_weighting.fill_bandwidth_neighbors | 1 | int >= 1 | `k` for the k-nearest-neighbor bandwidth selector. Ignored when `fill_method` not in {`gaussian`,`epsilon_gaussian`} or `fill_bandwidth != "knn"`. |
-| freqai.label_weighting.fill_bandwidth_alpha | 0.5 | float > 0 | Multiplicative factor on the k-th neighbor distance. Smaller values produce sharper, more separated Gaussians; larger values approach the `fixed` behavior. Ignored when `fill_method` not in {`gaussian`,`epsilon_gaussian`} or `fill_bandwidth != "knn"`. |
-| freqai.label_weighting.support_policy | `fallback` | enum {`fallback`,`raise`} | Policy when active label weighting fails support checks (after row filtering and causal split guards). `raise` aborts the fit with `ValueError`; `fallback` logs a `WARNING` and uses sanitized base sample weights for that fit. Eval (test/val) weights bypass this policy and always fall back on composition errors. |
-| freqai.label_weighting.min_pivot_equivalent_count | 3 | int >= 1 | Minimum number of surviving pivot-equivalent label weights required after filtering. Pivot-equivalent rows are weights at least 10% of the surviving maximum label weight. |
-| freqai.label_weighting.min_positive_label_weight_fraction | 0.01 | float [0,1] | Minimum fraction of filtered training rows with finite positive label weights. |
-| freqai.label_weighting.min_effective_sample_size | 3.0 | float >= 1 | Minimum Kish effective sample size of the final composed training weights. |
-| _Label pipeline_ | | | |
-| freqai.label_pipeline.standardization | `none` | enum {`none`,`zscore`,`robust`,`mmad`,`power_yj`} | Standardization method applied to labels before normalization. `none`=w, `zscore`=(w-μ)/σ, `robust`=(w-median)/(Q₃-Q₁), `mmad`=(w-median)/(MAD·k), `power_yj`=YJ(w). |
-| freqai.label_pipeline.robust_quantiles | [0.25, 0.75] | list[float] where 0 <= Q1 < Q3 <= 1 | Quantile range for robust standardization, Q1 and Q3. |
-| freqai.label_pipeline.mmad_scaling_factor | 1.4826 | float > 0 | Scaling factor for MMAD standardization. |
-| freqai.label_pipeline.normalization | `maxabs` | enum {`maxabs`,`minmax`,`sigmoid`,`none`} | Normalization method applied to labels. `maxabs`=w/max(\|w\|), `minmax`=low+(w-min)/(max-min)·(high-low), `sigmoid`=2·σ(scale·w)-1, `none`=w. |
-| freqai.label_pipeline.minmax_range | [-1.0, 1.0] | list[float] | Target range for `minmax` normalization, min and max. |
-| freqai.label_pipeline.sigmoid_scale | 1.0 | float > 0 | Scale parameter for `sigmoid` normalization, controls steepness. |
-| freqai.label_pipeline.gamma | 1.0 | float (0,10] | Contrast exponent applied to labels after normalization: >1 emphasizes extrema, values between 0 and 1 soften. |
-| _Feature parameters_ | | | |
-| freqai.feature_parameters.label_period_candles | min/max midpoint | int >= 1 | Zigzag labeling NATR period. |
-| freqai.feature_parameters.label_horizon_candles | `label_period_candles` | int >= 1 | Number of candles after a label row before the label is considered known by causal split guards. Recommended: cover the zigzag pivot confirmation lag (the smoothing kernel half-width is added automatically by `set_freqai_targets`). Used by causal split guards and `<label>_known_at_lookahead` metadata. When unset, falls back to `label_period_candles`. |
-| freqai.feature_parameters.causal_mode | true | bool | Causal split guard toggle. When `true` (default): rejects `data_split_parameters.shuffle=true`, `shuffle_after_split=true`, `reverse_train_test_order=true`; for `timeseries_split` auto-sets `gap=label_horizon_candles` when unset/`0` (rejects explicit `gap<label_horizon_candles`); for `train_test_split` drops train rows where position `>=first_test_position-label_horizon_candles`; with `<label>_known_at_lookahead` columns, additionally drops rows where `local_position + row-wise max(<label>_known_at_lookahead) >= first_test_position`. `false` is deprecated; acausal baselines only. |
-| freqai.feature_parameters.min_label_period_candles | 12 | int >= 1 | Minimum labeling NATR period used for reversals labeling HPO. |
-| freqai.feature_parameters.max_label_period_candles | 24 | int >= 1 | Maximum labeling NATR period used for reversals labeling HPO. |
-| freqai.feature_parameters.label_natr_multiplier | min/max midpoint | float > 0 | Zigzag labeling NATR multiplier. |
-| freqai.feature_parameters.min_label_natr_multiplier | 9.0 | float > 0 | Minimum labeling NATR multiplier used for reversals labeling HPO. |
-| freqai.feature_parameters.max_label_natr_multiplier | 12.0 | float > 0 | Maximum labeling NATR multiplier 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_weights | [1/7,1/7,1/7,1/7,1/7,1/7,1/7] | list[float] | Per-objective weights for trial selection methods. Objectives: (1) number of detected reversals, (2) median swing amplitude, (3) median (swing amplitude / median volatility-threshold ratio), (4) median swing volume per candle, (5) median swing speed, (6) median swing efficiency ratio, (7) median swing volume-weighted efficiency ratio. |
-| freqai.feature_parameters.label_p_order | None | float \| None | Lp exponent for parameterized metrics. Used by `minkowski` distance (default 2.0) and `power_mean` aggregation (default 1.0). Ignored by other metrics. |
-| freqai.feature_parameters.label_method | `compromise_programming` | enum {`compromise_programming`,`topsis`,`kmeans`,`kmeans2`,`kmedoids`,`knn`,`medoid`} | HPO `label` Pareto front trial selection method. |
-| freqai.feature_parameters.label_distance_metric | `euclidean` | enum {`euclidean`,`minkowski`,`chebyshev`,`cityblock`,`sqeuclidean`,`seuclidean`,`mahalanobis`,`harmonic_mean`,`geometric_mean`,`arithmetic_mean`,`quadratic_mean`,`cubic_mean`,`power_mean`,`weighted_sum`} | Distance metric for `compromise_programming` and `topsis` methods. Invalid values warn and fall back to `euclidean`. |
-| freqai.feature_parameters.label_cluster_metric | `euclidean` | enum {`euclidean`,`minkowski`,`chebyshev`,`cityblock`,`sqeuclidean`,`seuclidean`,`mahalanobis`} | Distance metric for `kmeans`, `kmeans2`, and `kmedoids` methods. Invalid values warn and fall back to `euclidean`. |
-| freqai.feature_parameters.label_cluster_selection_method | `topsis` | enum {`compromise_programming`,`topsis`} | Cluster selection method for clustering-based label methods. |
-| freqai.feature_parameters.label_cluster_trial_selection_method | `topsis` | enum {`compromise_programming`,`topsis`} | Best cluster trial selection method for clustering-based label methods. |
-| freqai.feature_parameters.label_density_metric | method-dependent | enum {`euclidean`,`minkowski`,`chebyshev`,`cityblock`,`sqeuclidean`,`seuclidean`,`mahalanobis`} | Distance metric for `knn` and `medoid` methods. Invalid values warn and fall back to the method's natural default (`minkowski` for `knn`, `euclidean` for `medoid`). |
-| freqai.feature_parameters.label_density_aggregation | `power_mean` | enum {`power_mean`,`quantile`,`min`,`max`} | Aggregation method for KNN neighbor distances. |
-| freqai.feature_parameters.label_density_n_neighbors | 5 | int >= 1 | Number of neighbors for KNN. |
-| freqai.feature_parameters.label_density_aggregation_param | aggregation-dependent | float \| None | Tunable for KNN neighbor distance aggregation: Lp exponent (`power_mean`) or quantile value (`quantile`). |
-| freqai.feature_parameters.scaler | `minmax` | enum {`minmax`,`maxabs`,`standard`,`robust`} | Feature scaling method. `minmax`=MinMaxScaler, `maxabs`=MaxAbsScaler, `standard`=StandardScaler, `robust`=RobustScaler. Changing this parameter requires deleting trained models. |
-| freqai.feature_parameters.range | [-1.0, 1.0] | list[float] | Target range for `minmax` scaler, min and max. Changing this parameter requires deleting trained models. |
-| _Label prediction_ | | | |
-| freqai.label_prediction.method | `thresholding` | enum {`none`,`thresholding`} | Prediction method. `none` disables threshold computation, `thresholding` enables adaptive threshold calculation. |
-| freqai.label_prediction.selection_method | `rank_extrema` | enum {`rank_extrema`,`rank_peaks`,`partition`} | Extrema selection method. `rank_extrema` ranks extrema values, `rank_peaks` ranks detected peak values, `partition` uses sign-based partitioning. |
-| freqai.label_prediction.threshold_method | `mean` | enum {`mean`,`isodata`,`li`,`minimum`,`otsu`,`triangle`,`yen`,`median`,`soft_extremum`} | Thresholding method for prediction thresholds. |
-| freqai.label_prediction.soft_extremum_alpha | 12.0 | float >= 0 | Alpha for `soft_extremum` threshold method. |
-| freqai.label_prediction.outlier_quantile | 0.999 | float (0,1) | Quantile threshold for predictions outlier filtering. |
-| freqai.label_prediction.keep_fraction | 0.0075 | float (0,1] | Fraction of extrema used for thresholds. 1 uses all, lower values keep only most significant. Applies to `rank_extrema` and `rank_peaks`; ignored for `partition`. |
-| _Optuna / HPO_ | | | |
-| freqai.optuna_hyperopt.enabled | false | bool | Enables regressor and dynamic label HPO. |
-| freqai.optuna_hyperopt.sampler | `tpe` | enum {`tpe`,`auto`} | HPO sampler algorithm for `hp` namespace. `tpe` uses [TPESampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.TPESampler.html) with multivariate, group, and constant_liar (when multiple workers), `auto` uses [AutoSampler](https://hub.optuna.org/samplers/auto_sampler). |
-| freqai.optuna_hyperopt.label_sampler | `auto` | enum {`auto`,`tpe`,`nsgaii`,`nsgaiii`} | HPO sampler algorithm for multi-objective `label` namespace. `nsgaii` uses [NSGAIISampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.NSGAIISampler.html), `nsgaiii` uses [NSGAIIISampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.NSGAIIISampler.html). |
-| freqai.optuna_hyperopt.storage | `file` | enum {`file`,`sqlite`} | HPO storage backend. |
-| freqai.optuna_hyperopt.continuous | true | bool | Continuous HPO. Forced for both namespaces in backtest and hyperopt, resetting the study on each optimization. |
-| freqai.optuna_hyperopt.warm_start | true | bool | Warm start HPO with previous best value(s). Persisted values are loaded and saved only in live and dry-run modes; non-live runs reuse only values produced earlier in the same run. |
-| freqai.optuna_hyperopt.n_startup_trials | 15 | int >= 0 | HPO startup trials. |
-| freqai.optuna_hyperopt.n_trials | 50 | int >= 1 | Maximum HPO trials. |
-| freqai.optuna_hyperopt.n_jobs | CPU threads / 4 | int >= 1 | Parallel HPO workers. |
-| freqai.optuna_hyperopt.timeout | 7200 | int >= 0 | HPO wall-clock timeout in seconds. |
-| freqai.optuna_hyperopt.label_candles_step | 1 | int >= 1 | Step for Zigzag NATR period `label` search space. |
-| freqai.optuna_hyperopt.space_reduction | false | bool | Enable/disable `hp` search space reduction based on previous best parameters. |
-| freqai.optuna_hyperopt.space_fraction | 0.4 | float [0,1] | Fraction of the `hp` search space to use with `space_reduction`. Lower values create narrower search ranges around the best parameters. |
-| freqai.optuna_hyperopt.min_resource | 3 | int >= 1 | Minimum resource per [HyperbandPruner](https://optuna.readthedocs.io/en/stable/reference/generated/optuna.pruners.HyperbandPruner.html) rung. |
-| freqai.optuna_hyperopt.seed | 1 | int >= 0 | HPO RNG seed. |
+| Path | Default | Type / Range | Description |
+| -------------------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| _Protections_ | | | |
+| custom_protections.trade_duration_candles | 72 | int >= 1 | Estimated trade duration in candles. Scales protections stop duration candles and trade limit. |
+| custom_protections.lookback_period_fraction | 0.5 | float (0,1] | Fraction of `fit_live_predictions_candles` used to calculate `lookback_period_candles` for _MaxDrawdown_ and _StoplossGuard_ protections. |
+| custom_protections.cooldown.enabled | true | bool | Enable/disable _CooldownPeriod_ protection. |
+| custom_protections.cooldown.stop_duration_candles | 4 | int >= 1 | Number of candles to wait before allowing new trades after a trade is closed. |
+| custom_protections.drawdown.enabled | true | bool | Enable/disable _MaxDrawdown_ protection. |
+| custom_protections.drawdown.max_allowed_drawdown | 0.2 | float (0,1) | Maximum allowed drawdown. |
+| custom_protections.stoploss.enabled | true | bool | Enable/disable _StoplossGuard_ protection. |
+| _Leverage_ | | | |
+| leverage | `proposed_leverage` | float [1.0, max_leverage] | Leverage. Fallback to `proposed_leverage` for the pair. |
+| _Exit pricing_ | | | |
+| exit_pricing.trade_price_target_method | `moving_average` | enum {`moving_average`,`quantile_interpolation`,`weighted_average`} | Trade NATR computation method. |
+| exit_pricing.thresholds_calibration.decline_quantile | 0.5 | float (0,1) | PnL decline quantile threshold. |
+| _Reversal confirmation_ | | | |
+| reversal_confirmation.lookback_period_candles | 0 | int >= 0 | Prior confirming candles; 0 = none. |
+| reversal_confirmation.decay_fraction | 0.5 | float (0,1] | Geometric per-candle volatility adjusted reversal threshold relaxation factor. |
+| reversal_confirmation.min_natr_multiplier_fraction | 0.0095 | float [0,1] | Lower bound fraction for volatility adjusted reversal threshold. |
+| reversal_confirmation.max_natr_multiplier_fraction | 0.0125 | float [0,1] | Upper bound fraction (>= lower bound) for volatility adjusted reversal threshold. |
+| _Regressor model_ | | | |
+| freqai.regressor | `xgboost` | enum {`xgboost`,`lightgbm`,`histgradientboostingregressor`,`ngboost`,`catboost`} | Machine learning regressor algorithm. |
+| _Model training parameters_ | | | |
+| freqai.model_training_parameters.gpu_vram_gb | 80 | enum {8,10,12,16,24,32,40,48,64,80} | Available GPU VRAM (GB) for CatBoost, not total. Constrains `depth`, `border_count`, and `max_ctr_complexity` ranges. |
+| _Data split parameters_ | | | |
+| freqai.data_split_parameters.method | `train_test_split` | enum {`train_test_split`,`timeseries_split`} | Data splitting strategy. `train_test_split` for sequential split, `timeseries_split` for chronological split with configurable gap. |
+| freqai.data_split_parameters.test_size | 0.1 / None | float (0,1) \| int >= 1 \| None | Test set size. Float for fraction, int for count. Default: 0.1 for `train_test_split`, None for `timeseries_split` (sklearn dynamic sizing). |
+| freqai.data_split_parameters.n_splits | 5 | int >= 2 | Controls train/test proportions for `timeseries_split` (higher = larger train set). |
+| freqai.data_split_parameters.gap | 0 | int >= 0 | Samples to exclude between train/test for `timeseries_split`. When `0` and `causal_mode=true` (default), auto-set from `label_horizon_candles`; when `0` and `causal_mode=false`, auto-set from `label_period_candles`. Under `causal_mode=true`, an explicit `gap<label_horizon_candles` is rejected. |
+| freqai.data_split_parameters.max_train_size | None | int >= 1 \| None | Maximum training set size for `timeseries_split`. When set, creates a sliding window instead of expanding train set. None = no limit. |
+| _Label smoothing_ | | | |
+| freqai.label_smoothing.method | `gaussian` | enum {`none`,`gaussian`,`kaiser`,`kaiser_bessel_derived`,`triang`,`smm`,`sma`,`savgol`,`gaussian_filter1d`} | Label smoothing method (`kaiser_bessel_derived` uses an even-length Kaiser-Bessel-derived zero-phase kernel; `smm`=median, `sma`=mean, `savgol`=Savitzky–Golay). |
+| freqai.label_smoothing.window_candles | 5 | int >= 3 | Smoothing window length (candles). |
+| freqai.label_smoothing.beta | 8.0 | float > 0 | Shape parameter for `kaiser` and `kaiser_bessel_derived` kernels. |
+| freqai.label_smoothing.polyorder | 3 | int >= 0 | Polynomial order for `savgol` smoothing. |
+| freqai.label_smoothing.mode | `mirror` | enum {`mirror`,`constant`,`nearest`,`wrap`,`interp`} | Boundary mode for `savgol` and `gaussian_filter1d`. |
+| freqai.label_smoothing.sigma | 1.0 | float > 0 | Gaussian `sigma` for `gaussian_filter1d` smoothing. |
+| _Label weighting_ | | | |
+| freqai.label_weighting.strategy | `none` | enum {`none`,`uniform`,`amplitude`,`amplitude_threshold_ratio`,`volume_rate`,`speed`,`efficiency_ratio`,`volume_weighted_efficiency_ratio`,`combined`} | Label weighting metric: none (`none`), uniform unit weight on every detected pivot (`uniform`), swing amplitude (`amplitude`), swing amplitude / median volatility-threshold ratio (`amplitude_threshold_ratio`), swing volume per candle (`volume_rate`), swing speed (`speed`), swing efficiency ratio (`efficiency_ratio`), swing volume-weighted efficiency ratio (`volume_weighted_efficiency_ratio`), or combined metrics aggregation (`combined`). Switching between `none` and any other strategy requires deleting trained models to realign training emphasis. |
+| freqai.label_weighting.metric_coefficients | {} | dict[str, float] | Per-metric coefficients for `combined` strategy. Keys: `amplitude`, `amplitude_threshold_ratio`, `volume_rate`, `speed`, `efficiency_ratio`, `volume_weighted_efficiency_ratio`. |
+| freqai.label_weighting.aggregation | `arithmetic_mean` | enum {`arithmetic_mean`,`geometric_mean`,`harmonic_mean`,`quadratic_mean`,`weighted_median`,`softmax`} | Metric aggregation method for `combined` strategy. `arithmetic_mean`=(Σ(w·m)/Σ(w)), `geometric_mean`=(∏(m^w))^(1/Σw), `harmonic_mean`=Σ(w)/(Σ(w/m)), `quadratic_mean`=(Σ(w·m²)/Σ(w))^(1/2), `weighted_median`=Q₀.₅(m,w), `softmax`=Σ(m·s_i) where s_i=w_i·exp(m_i/T)/Σ(w_j·exp(m_j/T)). |
+| freqai.label_weighting.softmax_temperature | 1.0 | float > 0 | Temperature T for `softmax` aggregation, controls distribution sharpness. |
+| freqai.label_weighting.fill_method | `zero` | enum {`zero`,`epsilon`,`gaussian`,`epsilon_gaussian`} | Off-pivot weighting scheme. `zero` hard-zeros off-pivot rows; `epsilon` applies the epsilon floor `fill_epsilon * <fill_epsilon_baseline>(pivot_weights)`; `gaussian` applies per-pivot Gaussian bumps; `epsilon_gaussian` sums the `epsilon` floor and the `gaussian` bumps. Pivot rows take the max of their raw weight and the off-pivot field at their index (no-op for `zero`). Switching away from `zero` may require retuning tree-leaf regularization (`min_child_weight`, `lambda`) and resetting any prior Optuna study. Changing this parameter requires deleting trained models. |
+| freqai.label_weighting.fill_epsilon | 0.000001 | float [0,1] | Off-pivot fraction of the pivot baseline. Ignored when `fill_method` not in {`epsilon`,`epsilon_gaussian`}. |
+| freqai.label_weighting.fill_epsilon_baseline | `mean` | enum {`mean`,`median`} | Pivot baseline statistic. `mean` tracks central tendency; `median` is robust against pivot-weight skew. Ignored when `fill_method` not in {`epsilon`,`epsilon_gaussian`}. |
+| freqai.label_weighting.fill_sigma_candles | 25.0 | float >= 0.5 | Gaussian standard deviation in candles for the per-pivot bumps. Acts as the upper bound on per-pivot sigma when `fill_bandwidth == "knn"`. Lower bound 0.5 prevents severe underflow in the Gaussian tail. Ignored when `fill_method` not in {`gaussian`,`epsilon_gaussian`}. |
+| freqai.label_weighting.fill_sigma_min_candles | 0.5 | float >= 0.5 | Lower bound on per-pivot sigma in candles when `fill_bandwidth == "knn"`. Clipped to `fill_sigma_candles` when larger. Ignored when `fill_method` not in {`gaussian`,`epsilon_gaussian`} or `fill_bandwidth != "knn"`. |
+| freqai.label_weighting.fill_bandwidth | `fixed` | enum {`fixed`,`knn`} | Per-pivot Gaussian bandwidth selector. `fixed` applies a constant `fill_sigma_candles` to every pivot (legacy behavior). `knn` adapts each pivot's sigma to local pivot density via `sigma_p = clip(fill_bandwidth_alpha * d_k(p), fill_sigma_min_candles, fill_sigma_candles)` where `d_k(p)` is the index distance to the `k`-th nearest pivot neighbor (Loftsgaarden & Quesenberry 1965; Silverman 1986, §5.2). Mitigates the crushing of weaker pivots by stronger neighbors in dense clusters. Ignored when `fill_method` not in {`gaussian`,`epsilon_gaussian`}. |
+| freqai.label_weighting.fill_bandwidth_neighbors | 1 | int >= 1 | `k` for the k-nearest-neighbor bandwidth selector. Ignored when `fill_method` not in {`gaussian`,`epsilon_gaussian`} or `fill_bandwidth != "knn"`. |
+| freqai.label_weighting.fill_bandwidth_alpha | 0.5 | float > 0 | Multiplicative factor on the k-th neighbor distance. Smaller values produce sharper, more separated Gaussians; larger values approach the `fixed` behavior. Ignored when `fill_method` not in {`gaussian`,`epsilon_gaussian`} or `fill_bandwidth != "knn"`. |
+| freqai.label_weighting.support_policy | `fallback` | enum {`fallback`,`raise`} | Policy when active label weighting fails support checks (after row filtering and causal split guards). `raise` aborts the fit with `ValueError`; `fallback` logs a `WARNING` and uses sanitized base sample weights for that fit. Eval (test/val) weights bypass this policy and always fall back on composition errors. |
+| freqai.label_weighting.min_pivot_equivalent_count | 3 | int >= 1 | Minimum number of surviving pivot-equivalent label weights required after filtering. Pivot-equivalent rows are weights at least 10% of the surviving maximum label weight. |
+| freqai.label_weighting.min_positive_label_weight_fraction | 0.01 | float [0,1] | Minimum fraction of filtered training rows with finite positive label weights. |
+| freqai.label_weighting.min_effective_sample_size | 3.0 | float >= 1 | Minimum Kish effective sample size of the final composed training weights. |
+| _Label pipeline_ | | | |
+| freqai.label_pipeline.standardization | `none` | enum {`none`,`zscore`,`robust`,`mmad`,`power_yj`} | Standardization method applied to labels before normalization. `none`=w, `zscore`=(w-μ)/σ, `robust`=(w-median)/(Q₃-Q₁), `mmad`=(w-median)/(MAD·k), `power_yj`=YJ(w). |
+| freqai.label_pipeline.robust_quantiles | [0.25, 0.75] | list[float] where 0 <= Q1 < Q3 <= 1 | Quantile range for robust standardization, Q1 and Q3. |
+| freqai.label_pipeline.mmad_scaling_factor | 1.4826 | float > 0 | Scaling factor for MMAD standardization. |
+| freqai.label_pipeline.normalization | `maxabs` | enum {`maxabs`,`minmax`,`sigmoid`,`none`} | Normalization method applied to labels. `maxabs`=w/max(\|w\|), `minmax`=low+(w-min)/(max-min)·(high-low), `sigmoid`=2·σ(scale·w)-1, `none`=w. |
+| freqai.label_pipeline.minmax_range | [-1.0, 1.0] | list[float] | Target range for `minmax` normalization, min and max. |
+| freqai.label_pipeline.sigmoid_scale | 1.0 | float > 0 | Scale parameter for `sigmoid` normalization, controls steepness. |
+| freqai.label_pipeline.gamma | 1.0 | float (0,10] | Contrast exponent applied to labels after normalization: >1 emphasizes extrema, values between 0 and 1 soften. |
+| _Feature parameters_ | | | |
+| freqai.feature_parameters.label_period_candles | min/max midpoint | int >= 1 | Zigzag labeling NATR period. |
+| freqai.feature_parameters.label_horizon_candles | `label_period_candles` | int >= 1 | Conservative fixed purge used by causal train/test guards and as the default `timeseries_split` gap. Zigzag labels additionally expose their exact row-wise confirmation time; centered smoothing composes the maximum availability time across each kernel support. When unset, falls back to `label_period_candles`. |
+| freqai.feature_parameters.causal_mode | true | bool | Causal split guard toggle. When `true` (default): rejects `data_split_parameters.shuffle=true`, `shuffle_after_split=true`, `reverse_train_test_order=true`; for `timeseries_split` auto-sets `gap=label_horizon_candles` when unset/`0` (rejects explicit `gap<label_horizon_candles`); for `train_test_split` applies the same fixed purge; both split methods additionally remove train rows whose exact Zigzag confirmation (and, under active weighting, one-pivot-later weight availability) plus smoothing availability reaches the test boundary. `false` is deprecated; acausal baselines only. |
+| freqai.feature_parameters.min_label_period_candles | 12 | int >= 1 | Minimum labeling NATR period used for reversals labeling HPO. |
+| freqai.feature_parameters.max_label_period_candles | 24 | int >= 1 | Maximum labeling NATR period used for reversals labeling HPO. |
+| freqai.feature_parameters.label_natr_multiplier | min/max midpoint | float > 0 | Zigzag labeling NATR multiplier. |
+| freqai.feature_parameters.min_label_natr_multiplier | 9.0 | float > 0 | Minimum labeling NATR multiplier used for reversals labeling HPO. |
+| freqai.feature_parameters.max_label_natr_multiplier | 12.0 | float > 0 | Maximum labeling NATR multiplier 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_weights | [1/7,1/7,1/7,1/7,1/7,1/7,1/7] | list[float] | Per-objective weights for trial selection methods. Objectives: (1) number of detected reversals, (2) median swing amplitude, (3) median (swing amplitude / median volatility-threshold ratio), (4) median swing volume per candle, (5) median swing speed, (6) median swing efficiency ratio, (7) median swing volume-weighted efficiency ratio. |
+| freqai.feature_parameters.label_p_order | None | float \| None | Lp exponent for parameterized metrics. Used by `minkowski` distance (default 2.0) and `power_mean` aggregation (default 1.0). Ignored by other metrics. |
+| freqai.feature_parameters.label_method | `compromise_programming` | enum {`compromise_programming`,`topsis`,`kmeans`,`kmeans2`,`kmedoids`,`knn`,`medoid`} | HPO `label` Pareto front trial selection method. |
+| freqai.feature_parameters.label_distance_metric | `euclidean` | enum {`euclidean`,`minkowski`,`chebyshev`,`cityblock`,`sqeuclidean`,`seuclidean`,`mahalanobis`,`harmonic_mean`,`geometric_mean`,`arithmetic_mean`,`quadratic_mean`,`cubic_mean`,`power_mean`,`weighted_sum`} | Distance metric for `compromise_programming` and `topsis` methods. Invalid values warn and fall back to `euclidean`. |
+| freqai.feature_parameters.label_cluster_metric | `euclidean` | enum {`euclidean`,`minkowski`,`chebyshev`,`cityblock`,`sqeuclidean`,`seuclidean`,`mahalanobis`} | Distance metric for `kmeans`, `kmeans2`, and `kmedoids` methods. Invalid values warn and fall back to `euclidean`. |
+| freqai.feature_parameters.label_cluster_selection_method | `topsis` | enum {`compromise_programming`,`topsis`} | Cluster selection method for clustering-based label methods. |
+| freqai.feature_parameters.label_cluster_trial_selection_method | `topsis` | enum {`compromise_programming`,`topsis`} | Best cluster trial selection method for clustering-based label methods. |
+| freqai.feature_parameters.label_density_metric | method-dependent | enum {`euclidean`,`minkowski`,`chebyshev`,`cityblock`,`sqeuclidean`,`seuclidean`,`mahalanobis`} | Distance metric for `knn` and `medoid` methods. Invalid values warn and fall back to the method's natural default (`minkowski` for `knn`, `euclidean` for `medoid`). |
+| freqai.feature_parameters.label_density_aggregation | `power_mean` | enum {`power_mean`,`quantile`,`min`,`max`} | Aggregation method for KNN neighbor distances. |
+| freqai.feature_parameters.label_density_n_neighbors | 5 | int >= 1 | Number of neighbors for KNN. |
+| freqai.feature_parameters.label_density_aggregation_param | aggregation-dependent | float \| None | Tunable for KNN neighbor distance aggregation: Lp exponent (`power_mean`) or quantile value (`quantile`). |
+| freqai.feature_parameters.scaler | `minmax` | enum {`minmax`,`maxabs`,`standard`,`robust`} | Feature scaling method. `minmax`=MinMaxScaler, `maxabs`=MaxAbsScaler, `standard`=StandardScaler, `robust`=RobustScaler. Changing this parameter requires deleting trained models. |
+| freqai.feature_parameters.range | [-1.0, 1.0] | list[float] | Target range for `minmax` scaler, min and max. Changing this parameter requires deleting trained models. |
+| _Label prediction_ | | | |
+| freqai.label_prediction.method | `thresholding` | enum {`none`,`thresholding`} | Prediction method. `none` disables threshold computation, `thresholding` enables adaptive threshold calculation. |
+| freqai.label_prediction.selection_method | `rank_extrema` | enum {`rank_extrema`,`rank_peaks`,`partition`} | Extrema selection method. `rank_extrema` ranks extrema values, `rank_peaks` ranks detected peak values, `partition` uses sign-based partitioning. |
+| freqai.label_prediction.threshold_method | `mean` | enum {`mean`,`isodata`,`li`,`minimum`,`otsu`,`triangle`,`yen`,`median`,`soft_extremum`} | Thresholding method for prediction thresholds. |
+| freqai.label_prediction.soft_extremum_alpha | 12.0 | float >= 0 | Alpha for `soft_extremum` threshold method. |
+| freqai.label_prediction.outlier_quantile | 0.999 | float (0,1) | Quantile threshold for predictions outlier filtering. |
+| freqai.label_prediction.keep_fraction | 0.0075 | float (0,1] | Fraction of extrema used for thresholds. 1 uses all, lower values keep only most significant. Applies to `rank_extrema` and `rank_peaks`; ignored for `partition`. |
+| _Optuna / HPO_ | | | |
+| freqai.optuna_hyperopt.enabled | false | bool | Enables regressor and dynamic label HPO. |
+| freqai.optuna_hyperopt.sampler | `tpe` | enum {`tpe`,`auto`} | HPO sampler algorithm for `hp` namespace. `tpe` uses [TPESampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.TPESampler.html) with multivariate, group, and constant_liar (when multiple workers), `auto` uses [AutoSampler](https://hub.optuna.org/samplers/auto_sampler). |
+| freqai.optuna_hyperopt.label_sampler | `auto` | enum {`auto`,`tpe`,`nsgaii`,`nsgaiii`} | HPO sampler algorithm for multi-objective `label` namespace. `nsgaii` uses [NSGAIISampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.NSGAIISampler.html), `nsgaiii` uses [NSGAIIISampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.NSGAIIISampler.html). |
+| freqai.optuna_hyperopt.storage | `file` | enum {`file`,`sqlite`} | HPO storage backend. |
+| freqai.optuna_hyperopt.continuous | true | bool | Continuous HPO. Forced for both namespaces in backtest and hyperopt, resetting the study on each optimization. |
+| freqai.optuna_hyperopt.warm_start | true | bool | Warm start HPO with previous best value(s). Persisted values are loaded and saved only in live and dry-run modes; non-live runs reuse only values produced earlier in the same run. |
+| freqai.optuna_hyperopt.n_startup_trials | 15 | int >= 0 | HPO startup trials. |
+| freqai.optuna_hyperopt.n_trials | 50 | int >= 1 | Maximum HPO trials. |
+| freqai.optuna_hyperopt.n_jobs | CPU threads / 4 | int >= 1 | Parallel HPO workers. |
+| freqai.optuna_hyperopt.timeout | 7200 | int >= 0 | HPO wall-clock timeout in seconds. |
+| freqai.optuna_hyperopt.label_candles_step | 1 | int >= 1 | Step for Zigzag NATR period `label` search space. |
+| freqai.optuna_hyperopt.space_reduction | false | bool | Enable/disable `hp` search space reduction based on previous best parameters. |
+| freqai.optuna_hyperopt.space_fraction | 0.4 | float [0,1] | Fraction of the `hp` search space to use with `space_reduction`. Lower values create narrower search ranges around the best parameters. |
+| freqai.optuna_hyperopt.min_resource | 3 | int >= 1 | Minimum resource per [HyperbandPruner](https://optuna.readthedocs.io/en/stable/reference/generated/optuna.pruners.HyperbandPruner.html) rung. |
+| freqai.optuna_hyperopt.seed | 1 | int >= 0 | HPO RNG seed. |
## ReforceXY
_LABEL_WEIGHT_SUFFIX: Final[str] = "_weight"
_LABEL_KNOWN_AT_LOOKAHEAD_SUFFIX: Final[str] = "_known_at_lookahead"
+_LABEL_WEIGHT_KNOWN_AT_LOOKAHEAD_SUFFIX: Final[str] = "_weight_known_at_lookahead"
LABEL_COLUMNS: Final[tuple[str, ...]] = (EXTREMA_COLUMN,)
return _label_aux_column_name(label_col, _LABEL_KNOWN_AT_LOOKAHEAD_SUFFIX)
+def label_weight_known_at_lookahead_column_name(label_col: str) -> str:
+ """Return the weight-availability lookahead column name for ``label_col``."""
+ return _label_aux_column_name(label_col, _LABEL_WEIGHT_KNOWN_AT_LOOKAHEAD_SUFFIX)
+
+
@dataclass
class LabelData:
"""Output of a label generator.
) -> LabelData:
natr_period = params.get("natr_period", 14)
natr_multiplier = params.get("natr_multiplier", 9.0)
- label_horizon_candles = get_label_horizon_candles(params, logger)
-
- (
- pivots_indices,
- _,
- pivots_directions,
- pivots_amplitudes,
- pivots_amplitude_threshold_ratios,
- pivots_volume_rates,
- pivots_speeds,
- pivots_efficiency_ratios,
- pivots_volume_weighted_efficiency_ratios,
- ) = zigzag(
+ result = _zigzag(
dataframe,
natr_period=natr_period,
natr_multiplier=natr_multiplier,
)
series = pd.Series(0.0, index=dataframe.index)
- if pivots_indices:
- series.loc[pivots_indices] = pivots_directions
+ if result.indices:
+ series.loc[result.indices] = result.directions
metrics: dict[str, list[float]] = {
- "amplitude": pivots_amplitudes,
- "amplitude_threshold_ratio": pivots_amplitude_threshold_ratios,
- "volume_rate": pivots_volume_rates,
- "speed": pivots_speeds,
- "efficiency_ratio": pivots_efficiency_ratios,
- "volume_weighted_efficiency_ratio": pivots_volume_weighted_efficiency_ratios,
+ "amplitude": result.amplitudes,
+ "amplitude_threshold_ratio": result.amplitude_threshold_ratios,
+ "volume_rate": result.volume_rates,
+ "speed": result.speeds,
+ "efficiency_ratio": result.efficiency_ratios,
+ "volume_weighted_efficiency_ratio": result.volume_weighted_efficiency_ratios,
}
- # Per-row label lookahead (in candles), NOT an absolute position:
- # freqtrade's ``dk.slice_dataframe`` runs AFTER ``set_freqai_targets``,
- # so any pre-slice absolute position would no longer match the causal
- # guard's local ``np.arange(len(unfiltered_df))`` coordinate system.
known_at_lookahead = pd.Series(
- int(label_horizon_candles),
+ result.known_at_positions - np.arange(len(dataframe), dtype=np.int64),
index=dataframe.index,
dtype=np.int64,
)
return LabelData(
series=series,
- indices=pivots_indices,
+ indices=result.indices,
metrics=metrics,
known_at_lookahead=known_at_lookahead,
)
return effective_window // 2
+def _sanitize_known_at_lookahead(
+ known_at_lookahead: pd.Series,
+) -> tuple[NDArray[np.int64], NDArray[np.int64]]:
+ """Row positions and non-finite-guarded int64 lookahead values.
+
+ Non-finite lookahead casts to garbage int64 (silent, with a RuntimeWarning)
+ and a spurious ``0`` reads as "available now", keeping a row whose
+ availability is actually unknown. Map non-finite so its known_at position is
+ ``n`` (maximally unavailable) and such rows are always purged near a boundary.
+ """
+ n = len(known_at_lookahead)
+ positions = np.arange(n, dtype=np.int64)
+ raw = known_at_lookahead.to_numpy(dtype=float)
+ if n and not np.isfinite(raw).all():
+ raw = np.where(np.isfinite(raw), raw, (n - positions).astype(float))
+ return positions, raw.astype(np.int64)
+
+
+def compose_label_lookahead(
+ known_at_lookahead: pd.Series,
+ kernel_half_width: int,
+) -> pd.Series:
+ """Compose row-wise label availability with a centered smoothing kernel.
+
+ Values within the current frame's right kernel boundary stay unavailable
+ until enough future rows exist to provide the complete smoothing window.
+ """
+ if known_at_lookahead.empty:
+ return known_at_lookahead.copy()
+ n = len(known_at_lookahead)
+ positions, known_at_lookahead_values = _sanitize_known_at_lookahead(
+ known_at_lookahead
+ )
+ if kernel_half_width <= 0:
+ return pd.Series(
+ known_at_lookahead_values,
+ index=known_at_lookahead.index,
+ dtype=np.int64,
+ )
+ known_at_positions = pd.Series(
+ positions + known_at_lookahead_values,
+ index=known_at_lookahead.index,
+ )
+ smoothed_known_at_positions = known_at_positions.rolling(
+ window=2 * kernel_half_width + 1,
+ center=True,
+ min_periods=1,
+ ).max()
+ right_edge_start = max(0, n - kernel_half_width)
+ smoothed_known_at_positions.iloc[right_edge_start:] = n
+ return pd.Series(
+ smoothed_known_at_positions.to_numpy(dtype=np.int64) - positions,
+ index=known_at_lookahead.index,
+ dtype=np.int64,
+ )
+
+
TradePriceTarget = Literal[
"moving_average", "quantile_interpolation", "weighted_average"
]
)
+# k for the weight-fill availability band: rows within k*fill_sigma_candles of a
+# pivot receive its Gaussian bump. Material radius (bump > 1% of pivot peak) is
+# sqrt(2*ln(100))=3.0349 sigma; k=4 -> tail exp(-8)=3.4e-4, ~30x below 1% with a
+# margin robust to sigma rounding (k=3 under-covers for larger sigma).
+_WEIGHT_FILL_RADIUS_SIGMA_MULTIPLIER: Final[float] = 4.0
+
+
+def weight_fill_radius(weighting_config: dict[str, Any]) -> int:
+ """Row radius over which a pivot's Gaussian-fill weight is causally shared.
+
+ Zero unless the off-pivot fill spreads a pivot's weight into neighbors
+ (``gaussian``/``epsilon_gaussian``). ``fill_sigma_candles`` upper-bounds the
+ per-pivot sigma (including ``knn``, which clips below it), so
+ ``ceil(k*fill_sigma_candles)`` covers every pivot's material Gaussian
+ support. The additive epsilon floor is a global O(fill_epsilon) term, left
+ unconstrained (negligible).
+ """
+ label_weighting = {**DEFAULTS_LABEL_WEIGHTING, **weighting_config}
+ if label_weighting["fill_method"] not in (
+ FILL_METHODS[2], # "gaussian"
+ FILL_METHODS[3], # "epsilon_gaussian"
+ ):
+ return 0
+ return math.ceil(
+ _WEIGHT_FILL_RADIUS_SIGMA_MULTIPLIER
+ * float(label_weighting["fill_sigma_candles"])
+ )
+
+
+def compute_label_weight_known_at_lookahead(
+ known_at_lookahead: pd.Series,
+ indices: Sequence[int] | NDArray[np.integer],
+ fill_radius: int = 0,
+) -> pd.Series:
+ """Per-row causal availability (in candles) of the label WEIGHT column.
+
+ A pivot's swing metric (its weight source) is backfilled from the adjacent
+ closing pivot, so it only becomes computable at the next pivot's
+ confirmation ``i_{k+1} == known_at_positions[indices[k+1]]``, one pivot
+ later than the pivot's own label availability ``i_k``. The trailing pivot has
+ no closing swing (weight 0 via ``_impute_weights``, so it never resolves
+ in-frame -> ``n``). Pivot rows are bumped to that lag; off-pivot rows keep
+ their label availability, EXCEPT that a Gaussian fill spreads each pivot's
+ weight over a local band ``[idx-fill_radius, idx+fill_radius]``
+ (0 disables), whose availability is bounded too. The band is LOCAL per pivot,
+ never a global max (a global bound forces ``n`` on all rows -> total train
+ purge). Folded via ``max(label, weight)`` by the causal purge.
+
+ The ``i_{k+1}`` availability is exact for ``fill_bandwidth='fixed'`` and for
+ ``knn`` with ``fill_bandwidth_neighbors=1`` (a pivot sigma then depends only
+ on already-confirmed adjacent pivots), up to each pivot's material Gaussian
+ support (the tail beyond ``fill_radius`` is immaterial by design, see
+ ``weight_fill_radius``). Under ``knn`` with ``fill_bandwidth_neighbors>=2`` a
+ pivot sigma can depend on a k-th nearest neighbor confirmed after ``i_{k+1}``,
+ making ``i_{k+1}`` a lower bound. This understates the band availability only
+ when that neighbor also lies outside ``fill_radius``, which requires
+ ``fill_bandwidth_alpha < 0.25`` (otherwise ``fill_radius = ceil(4 *
+ fill_sigma_candles)`` covers it and the neighbor's own later availability
+ dominates the ``max`` fold): the understatement is nil at the default
+ ``fill_bandwidth_alpha=0.5`` and can otherwise reach a large fraction of the
+ peak weight on the affected rows. Prefer ``fill_bandwidth='fixed'``,
+ ``fill_bandwidth_neighbors=1``, or ``fill_bandwidth_alpha>=0.25`` for
+ exactness.
+ """
+ n = len(known_at_lookahead)
+ positions, known_at_lookahead_values = _sanitize_known_at_lookahead(
+ known_at_lookahead
+ )
+ if n == 0:
+ return pd.Series(positions, index=known_at_lookahead.index, dtype=np.int64)
+ known_at_positions = positions + known_at_lookahead_values
+ idx = np.asarray(indices, dtype=int)
+ idx = np.sort(idx[(idx >= 0) & (idx < n)])
+ base = known_at_positions.copy()
+ if idx.size:
+ avail_pivot = np.empty(idx.size, dtype=np.int64)
+ avail_pivot[:-1] = known_at_positions[idx[1:]]
+ avail_pivot[-1] = n
+ base[idx] = np.maximum(base[idx], avail_pivot)
+ if fill_radius > 0:
+ for pivot_pos, pivot_avail in zip(idx.tolist(), avail_pivot.tolist()):
+ # Skip any pivot whose weight never resolves in-frame (sentinel
+ # availability == n): its Gaussian bump is 0, so it contributes
+ # to no row (on real _zigzag output only the trailing pivot).
+ if pivot_avail >= n:
+ continue
+ lo = max(0, pivot_pos - fill_radius)
+ hi = min(n, pivot_pos + fill_radius + 1)
+ np.maximum(base[lo:hi], pivot_avail, out=base[lo:hi])
+ base = np.clip(base, positions, n)
+ return pd.Series(base - positions, index=known_at_lookahead.index, dtype=np.int64)
+
+
def get_callable_sha256(fn: Callable[..., Any]) -> str:
if not callable(fn):
raise ValueError(f"Invalid fn value {type(fn).__name__!r}: must be callable")
DOWN = -1
-def zigzag(
- df: pd.DataFrame,
- natr_period: int = 14,
- natr_multiplier: float = 9.0,
- normalize: bool = False,
- *,
- logger: Logger | None = None,
-) -> tuple[
+ZigzagTuple = tuple[
list[int],
list[float],
list[TrendDirection],
list[float],
list[float],
list[float],
-]:
+]
+
+
+@dataclass(frozen=True, slots=True)
+class ZigzagResult:
+ indices: list[int]
+ values_log: list[float]
+ directions: list[TrendDirection]
+ amplitudes: list[float]
+ amplitude_threshold_ratios: list[float]
+ volume_rates: list[float]
+ speeds: list[float]
+ efficiency_ratios: list[float]
+ volume_weighted_efficiency_ratios: list[float]
+ known_at_positions: NDArray[np.integer]
+
+ def as_tuple(self) -> ZigzagTuple:
+ """Return the stable public tuple representation."""
+ return (
+ self.indices,
+ self.values_log,
+ self.directions,
+ self.amplitudes,
+ self.amplitude_threshold_ratios,
+ self.volume_rates,
+ self.speeds,
+ self.efficiency_ratios,
+ self.volume_weighted_efficiency_ratios,
+ )
+
+
+def _zigzag(
+ df: pd.DataFrame,
+ natr_period: int = 14,
+ natr_multiplier: float = 9.0,
+ normalize: bool = False,
+ *,
+ logger: Logger | None = None,
+) -> ZigzagResult:
n = len(df)
if df.empty or n < natr_period:
- return (
- [],
- [],
- [],
- [],
- [],
- [],
- [],
- [],
- [],
+ return ZigzagResult(
+ indices=[],
+ values_log=[],
+ directions=[],
+ amplitudes=[],
+ amplitude_threshold_ratios=[],
+ volume_rates=[],
+ speeds=[],
+ efficiency_ratios=[],
+ volume_weighted_efficiency_ratios=[],
+ known_at_positions=np.full(n, n, dtype=np.int64),
)
- natr_values = (ta.NATR(df, timeperiod=natr_period).bfill() / 100.0).to_numpy()
+ natr = ta.NATR(df, timeperiod=natr_period) / 100.0
+ finite_natr_positions = np.flatnonzero(np.isfinite(natr.to_numpy(dtype=float)))
+ natr_warmup_end_pos = (
+ int(finite_natr_positions[0]) if finite_natr_positions.size > 0 else n
+ )
+ natr_values = natr.bfill().to_numpy()
indices: list[int] = df.index.tolist()
thresholds: NDArray[np.floating] = natr_values * natr_multiplier
pivots_speeds: list[float] = []
pivots_efficiency_ratios: list[float] = []
pivots_volume_weighted_efficiency_ratios: list[float] = []
+ known_at_positions: NDArray[np.integer] = np.full(n, n, dtype=np.int64)
+ last_resolved_pos = -1
+ latest_confirmation_pos = -1
last_pivot_pos: int = -1
candidate_pivot_pos: int = -1
return vw_net_move / vw_path_length
- def add_pivot(pos: int, value_log: float, direction: TrendDirection):
- nonlocal last_pivot_pos
+ def add_pivot(
+ pos: int,
+ value_log: float,
+ direction: TrendDirection,
+ confirmed_at_pos: int,
+ resolve_through_pos: int,
+ ) -> None:
+ nonlocal last_pivot_pos, last_resolved_pos, latest_confirmation_pos
+ # Monotonic confirmation watermark: a pivot replayed after the initial
+ # orientation (scan restarts at initial_pivot_pos+1, before the
+ # orientation confirmation candle i) must not claim availability earlier
+ # than i, since its label depends on that orientation. Fold the latest
+ # confirmation seen so far so known_at never understates it.
+ confirmed_at_pos = max(
+ confirmed_at_pos,
+ resolve_through_pos,
+ natr_warmup_end_pos,
+ latest_confirmation_pos,
+ )
+ latest_confirmation_pos = confirmed_at_pos
+ known_at_positions[last_resolved_pos + 1 : resolve_through_pos + 1] = (
+ confirmed_at_pos
+ )
+ last_resolved_pos = max(last_resolved_pos, resolve_through_pos)
if pivots_indices and indices[pos] == pivots_indices[-1]:
return
+ # These swing metrics are backfilled onto the previous pivot from the
+ # adjacent closing pivot, confirmed at this pivot's known_at. The weight
+ # is therefore causally available one pivot later than its label;
+ # compute_label_weight_known_at_lookahead derives that lag so the causal
+ # purge masks weights on max(label, weight) availability.
if (
pivots_values_log
and last_pivot_pos >= 0
)
if is_initial_high_move_significant and is_initial_low_move_significant:
if initial_move_from_high > initial_move_from_low:
- add_pivot(initial_high_pos, initial_high_log, TrendDirection.UP)
+ add_pivot(
+ initial_high_pos,
+ initial_high_log,
+ TrendDirection.UP,
+ i,
+ initial_high_pos,
+ )
state = TrendDirection.DOWN
break
else:
- add_pivot(initial_low_pos, initial_low_log, TrendDirection.DOWN)
+ add_pivot(
+ initial_low_pos,
+ initial_low_log,
+ TrendDirection.DOWN,
+ i,
+ initial_low_pos,
+ )
state = TrendDirection.UP
break
else:
if is_initial_high_move_significant:
- add_pivot(initial_high_pos, initial_high_log, TrendDirection.UP)
+ add_pivot(
+ initial_high_pos,
+ initial_high_log,
+ TrendDirection.UP,
+ i,
+ initial_high_pos,
+ )
state = TrendDirection.DOWN
break
elif is_initial_low_move_significant:
- add_pivot(initial_low_pos, initial_low_log, TrendDirection.DOWN)
+ add_pivot(
+ initial_low_pos,
+ initial_low_log,
+ TrendDirection.DOWN,
+ i,
+ initial_low_pos,
+ )
state = TrendDirection.UP
break
else:
- return (
- [],
- [],
- [],
- [],
- [],
- [],
- [],
- [],
- [],
+ return ZigzagResult(
+ indices=[],
+ values_log=[],
+ directions=[],
+ amplitudes=[],
+ amplitude_threshold_ratios=[],
+ volume_rates=[],
+ speeds=[],
+ efficiency_ratios=[],
+ volume_weighted_efficiency_ratios=[],
+ known_at_positions=known_at_positions,
)
for i in range(last_pivot_pos + 1, n):
candidate_pivot_pos,
highs_log[candidate_pivot_pos],
TrendDirection.UP,
+ i,
+ i,
)
state = TrendDirection.DOWN
candidate_pivot_pos,
lows_log[candidate_pivot_pos],
TrendDirection.DOWN,
+ i,
+ i,
)
state = TrendDirection.UP
- if normalize:
- return (
- pivots_indices,
- pivots_values_log,
- pivots_directions,
- minmax_scale(pivots_amplitudes),
- minmax_scale(pivots_amplitude_threshold_ratios),
- minmax_scale(pivots_volume_rates),
- minmax_scale(pivots_speeds),
- pivots_efficiency_ratios,
- pivots_volume_weighted_efficiency_ratios,
- )
- return (
- pivots_indices,
- pivots_values_log,
- pivots_directions,
- pivots_amplitudes,
- pivots_amplitude_threshold_ratios,
- pivots_volume_rates,
- pivots_speeds,
- pivots_efficiency_ratios,
- pivots_volume_weighted_efficiency_ratios,
+ return ZigzagResult(
+ indices=pivots_indices,
+ values_log=pivots_values_log,
+ directions=pivots_directions,
+ amplitudes=(
+ minmax_scale(pivots_amplitudes) if normalize else pivots_amplitudes
+ ),
+ amplitude_threshold_ratios=(
+ minmax_scale(pivots_amplitude_threshold_ratios)
+ if normalize
+ else pivots_amplitude_threshold_ratios
+ ),
+ volume_rates=(
+ minmax_scale(pivots_volume_rates) if normalize else pivots_volume_rates
+ ),
+ speeds=minmax_scale(pivots_speeds) if normalize else pivots_speeds,
+ efficiency_ratios=pivots_efficiency_ratios,
+ volume_weighted_efficiency_ratios=pivots_volume_weighted_efficiency_ratios,
+ known_at_positions=known_at_positions,
)
+def zigzag(
+ df: pd.DataFrame,
+ natr_period: int = 14,
+ natr_multiplier: float = 9.0,
+ normalize: bool = False,
+ *,
+ logger: Logger | None = None,
+) -> ZigzagTuple:
+ """Return Zigzag outputs while preserving the existing public API."""
+ return _zigzag(
+ df,
+ natr_period=natr_period,
+ natr_multiplier=natr_multiplier,
+ normalize=normalize,
+ logger=logger,
+ ).as_tuple()
+
+
Regressor = Literal[
"xgboost", "lightgbm", "histgradientboostingregressor", "ngboost", "catboost"
]