---
<!-- OPENSPEC:START -->
-
**Guardrails**
-
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
- Keep changes tightly scoped to the requested outcome.
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
**Steps**
Track these steps as TODOs and complete them one by one.
-
1. Read `changes/<id>/proposal.md`, `design.md` (if present), and `tasks.md` to confirm scope and acceptance criteria.
2. Work through tasks sequentially, keeping edits minimal and focused on the requested change.
3. Confirm completion before updating statuses—make sure every item in `tasks.md` is finished.
5. Reference `openspec list` or `openspec show <item>` when additional context is required.
**Reference**
-
- Use `openspec show <id> --json --deltas-only` if you need additional context from the proposal while implementing.
<!-- OPENSPEC:END -->
---
description: Archive a deployed OpenSpec change and update specs.
---
-
<ChangeId>
$ARGUMENTS
</ChangeId>
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
**Steps**
-
1. Determine the change ID to archive:
- If this prompt already includes a specific change ID (for example inside a `<ChangeId>` block populated by slash-command arguments), use that value after trimming whitespace.
- If the conversation references a change loosely (for example by title or summary), run `openspec list` to surface likely IDs, share the relevant candidates, and confirm which one the user intends.
5. Validate with `openspec validate --strict` and inspect with `openspec show <id>` if anything looks off.
**Reference**
-
- Use `openspec list` to confirm change IDs before archiving.
- Inspect refreshed specs with `openspec list --specs` and address any validation issues before handing off.
<!-- OPENSPEC:END -->
</UserRequest>
<!-- OPENSPEC:START -->
-
**Guardrails**
-
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
- Keep changes tightly scoped to the requested outcome.
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
- Do not write any code during the proposal stage. Only create design documents (proposal.md, tasks.md, design.md, and spec deltas). Implementation happens in the apply stage after approval.
**Steps**
-
1. Review `openspec/project.md`, run `openspec list` and `openspec list --specs`, and inspect related code or docs (e.g., via `rg`/`ls`) to ground the proposal in current behaviour; note any gaps that require clarification.
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, and `design.md` (when needed) under `openspec/changes/<id>/`.
3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
7. Validate with `openspec validate <id> --strict` and resolve every issue before sharing the proposal.
**Reference**
-
- Use `openspec show <id> --json --deltas-only` or `openspec show <spec> --type spec` to inspect details when validation fails.
- Search existing requirements with `rg -n "Requirement:|Scenario:" openspec/specs` before writing new ones.
- Explore the codebase with `rg <keyword>`, `ls`, or direct file reads so proposals align with current implementation realities.
<!-- OPENSPEC:END -->
-Open `@/.github/copilot-instructions.md`, read it and strictly follow the instructions.
+Open `@/.github/copilot-instructions.md`, read it and strictly follow the
+instructions.
### 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 | `moving_average` | enum {`moving_average`,`quantile_interpolation`,`weighted_average`} | Trade NATR computation method. |
-| exit_pricing.thresholds_calibration.decline_quantile | 0.75 | float (0,1) | PnL decline quantile threshold. |
-| _Reversal confirmation_ | | | |
-| reversal_confirmation.lookback_period | 0 | int >= 0 | Prior confirming candles; 0 = none. |
-| reversal_confirmation.decay_ratio | 0.5 | float (0,1] | Geometric per-candle volatility adjusted reversal threshold relaxation factor. |
-| reversal_confirmation.min_natr_ratio_percent | 0.0095 | float [0,1] | Lower bound fraction for volatility adjusted reversal threshold. |
-| reversal_confirmation.max_natr_ratio_percent | 0.075 | float [0,1] | Upper bound fraction (>= lower bound) for volatility adjusted reversal threshold. |
-| _Regressor model_ | | | |
-| freqai.regressor | `xgboost` | enum {`xgboost`,`lightgbm`,`histgradientboostingregressor`} | Machine learning regressor algorithm. |
-| _Extrema smoothing_ | | | |
-| freqai.extrema_smoothing.method | `gaussian` | enum {`gaussian`,`kaiser`,`triang`,`smm`,`sma`,`savgol`,`gaussian_filter1d`} | Extrema smoothing method (`smm`=median, `sma`=mean, `savgol`=Savitzky–Golay). |
-| freqai.extrema_smoothing.window | 5 | int >= 3 | Smoothing window length (candles). |
-| freqai.extrema_smoothing.beta | 8.0 | float > 0 | Shape parameter for `kaiser` kernel. |
-| freqai.extrema_smoothing.polyorder | 3 | int >= 1 | Polynomial order for `savgol` smoothing. |
-| freqai.extrema_smoothing.mode | `mirror` | enum {`mirror`,`constant`,`nearest`,`wrap`,`interp`} | Boundary mode for `savgol` and `gaussian_filter1d`. |
-| freqai.extrema_smoothing.sigma | 1.0 | float > 0 | Gaussian `sigma` for `gaussian_filter1d` smoothing. |
-| _Extrema weighting_ | | | |
-| freqai.extrema_weighting.strategy | `none` | enum {`none`,`amplitude`,`amplitude_threshold_ratio`,`volume_rate`,`speed`,`efficiency_ratio`,`volume_weighted_efficiency_ratio`,`hybrid`} | Extrema weighting source: unweighted (`none`), 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 `hybrid`. |
-| freqai.extrema_weighting.source_weights | `{}` | dict[str, float] | Weights on extrema weighting sources for `hybrid`. |
-| freqai.extrema_weighting.aggregation | `weighted_sum` | enum {`weighted_sum`,`geometric_mean`} | Aggregation method applied to weighted extrema weighting sources for `hybrid`. |
-| freqai.extrema_weighting.aggregation_normalization | `none` | enum {`minmax`,`sigmoid`,`softmax`,`l1`,`l2`,`rank`,`none`} | Normalization method applied to the aggregated extrema weighting source for `hybrid`. |
-| freqai.extrema_weighting.standardization | `none` | enum {`none`,`zscore`,`robust`,`mmad`} | Standardization method applied to weights before normalization. `none`=no standardization, `zscore`=(w-μ)/σ, `robust`=(w-median)/IQR, `mmad`=(w-median)/MAD. |
-| freqai.extrema_weighting.robust_quantiles | [0.25, 0.75] | list[float] where 0 <= Q1 < Q3 <= 1 | Quantile range for robust standardization, Q1 and Q3. |
-| freqai.extrema_weighting.mmad_scaling_factor | 1.4826 | float > 0 | Scaling factor for MMAD standardization. |
-| freqai.extrema_weighting.normalization | `minmax` | enum {`minmax`,`sigmoid`,`softmax`,`l1`,`l2`,`rank`,`none`} | Normalization method applied to weights. |
-| freqai.extrema_weighting.minmax_range | [0.0, 1.0] | list[float] | Target range for `minmax` normalization, min and max. |
-| freqai.extrema_weighting.sigmoid_scale | 1.0 | float > 0 | Scale parameter for `sigmoid` normalization, controls steepness. |
-| freqai.extrema_weighting.softmax_temperature | 1.0 | float > 0 | Temperature parameter for `softmax` normalization: lower values sharpen distribution, higher values flatten it. |
-| freqai.extrema_weighting.rank_method | `average` | enum {`average`,`min`,`max`,`dense`,`ordinal`} | Ranking method for `rank` normalization. |
-| freqai.extrema_weighting.gamma | 1.0 | float (0,10] | Contrast exponent applied 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 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 | [1/7,1/7,1/7,1/7,1/7,1/7,1/7] | list[float] | Per-objective weights used in distance calculations to ideal point. 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 | p-order used by `minkowski` / `power_mean` (optional). |
-| freqai.feature_parameters.label_medoid_metric | `euclidean` | string | Metric used with `medoid`. |
-| freqai.feature_parameters.label_kmeans_metric | `euclidean` | string | Metric used for k-means clustering. |
-| freqai.feature_parameters.label_kmeans_selection | `min` | enum {`min`,`medoid`} | Strategy to select trial in the best k-means cluster. |
-| freqai.feature_parameters.label_kmedoids_metric | `euclidean` | string | Metric used for k-medoids clustering. |
-| freqai.feature_parameters.label_kmedoids_selection | `min` | enum {`min`,`medoid`} | Strategy to select trial in the best k-medoids cluster. |
-| freqai.feature_parameters.label_knn_metric | `minkowski` | string | Distance metric for KNN. |
-| freqai.feature_parameters.label_knn_p_order | `None` | float \| None | Tunable for KNN neighbor distances aggregation methods: p-order (`knn_power_mean`, default: 1.0) or quantile (`knn_quantile`, default: 0.5). (optional) |
-| freqai.feature_parameters.label_knn_n_neighbors | 5 | int >= 1 | Number of neighbors for KNN. |
-| _Predictions extrema_ | | | |
-| freqai.predictions_extrema.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.predictions_extrema.thresholds_smoothing | `mean` | enum {`mean`,`isodata`,`li`,`minimum`,`otsu`,`triangle`,`yen`,`median`,`soft_extremum`} | Thresholding method for prediction thresholds smoothing. |
-| freqai.predictions_extrema.thresholds_alpha | 12.0 | float > 0 | Alpha for `soft_extremum` thresholds smoothing. |
-| freqai.predictions_extrema.threshold_outlier | 0.999 | float (0,1) | Quantile threshold for predictions outlier filtering. |
-| freqai.predictions_extrema.extrema_fraction | 1.0 | float (0,1] | Fraction of extrema used for thresholds. `1.0` 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 HPO. |
-| freqai.optuna_hyperopt.sampler | `tpe` | enum {`tpe`,`auto`} | HPO sampler algorithm. `tpe` uses [TPESampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.TPESampler.html) with multivariate and group, `auto` uses [AutoSampler](https://hub.optuna.org/samplers/auto_sampler). |
-| freqai.optuna_hyperopt.storage | `file` | enum {`file`,`sqlite`} | HPO storage backend. |
-| freqai.optuna_hyperopt.continuous | true | bool | Continuous HPO. |
-| freqai.optuna_hyperopt.warm_start | true | bool | Warm start HPO with previous best value(s). |
-| 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 horizon search space. |
-| freqai.optuna_hyperopt.train_candles_step | 10 | int >= 1 | Step for training sets size search space. |
-| freqai.optuna_hyperopt.space_reduction | false | bool | Enable/disable HPO search space reduction based on previous best parameters. |
-| freqai.optuna_hyperopt.expansion_ratio | 0.4 | float [0,1] | HPO search space expansion ratio. |
-| freqai.optuna_hyperopt.min_resource | 3 | int >= 1 | Minimum resource per Hyperband pruner 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. (Deprecated alias: `exit_pricing.trade_price_target`) |
+| exit_pricing.thresholds_calibration.decline_quantile | 0.75 | float (0,1) | PnL decline quantile threshold. |
+| _Reversal confirmation_ | | | |
+| reversal_confirmation.lookback_period_candles | 0 | int >= 0 | Prior confirming candles; 0 = none. (Deprecated alias: `reversal_confirmation.lookback_period`) |
+| reversal_confirmation.decay_fraction | 0.5 | float (0,1] | Geometric per-candle volatility adjusted reversal threshold relaxation factor. (Deprecated alias: `reversal_confirmation.decay_ratio`) |
+| reversal_confirmation.min_natr_multiplier_fraction | 0.0095 | float [0,1] | Lower bound fraction for volatility adjusted reversal threshold. (Deprecated alias: `reversal_confirmation.min_natr_ratio_percent`) |
+| reversal_confirmation.max_natr_multiplier_fraction | 0.075 | float [0,1] | Upper bound fraction (>= lower bound) for volatility adjusted reversal threshold. (Deprecated alias: `reversal_confirmation.max_natr_ratio_percent`) |
+| _Regressor model_ | | | |
+| freqai.regressor | `xgboost` | enum {`xgboost`,`lightgbm`,`histgradientboostingregressor`} | Machine learning regressor algorithm. |
+| _Extrema smoothing_ | | | |
+| freqai.extrema_smoothing.method | `gaussian` | enum {`gaussian`,`kaiser`,`triang`,`smm`,`sma`,`savgol`,`gaussian_filter1d`} | Extrema smoothing method (`smm`=median, `sma`=mean, `savgol`=Savitzky–Golay). |
+| freqai.extrema_smoothing.window_candles | 5 | int >= 3 | Smoothing window length (candles). (Deprecated alias: `freqai.extrema_smoothing.window`) |
+| freqai.extrema_smoothing.beta | 8.0 | float > 0 | Shape parameter for `kaiser` kernel. |
+| freqai.extrema_smoothing.polyorder | 3 | int >= 1 | Polynomial order for `savgol` smoothing. |
+| freqai.extrema_smoothing.mode | `mirror` | enum {`mirror`,`constant`,`nearest`,`wrap`,`interp`} | Boundary mode for `savgol` and `gaussian_filter1d`. |
+| freqai.extrema_smoothing.sigma | 1.0 | float > 0 | Gaussian `sigma` for `gaussian_filter1d` smoothing. |
+| _Extrema weighting_ | | | |
+| freqai.extrema_weighting.strategy | `none` | enum {`none`,`amplitude`,`amplitude_threshold_ratio`,`volume_rate`,`speed`,`efficiency_ratio`,`volume_weighted_efficiency_ratio`,`hybrid`} | Extrema weighting source: unweighted (`none`), 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 `hybrid`. |
+| freqai.extrema_weighting.source_weights | `{}` | dict[str, float] | Weights on extrema weighting sources for `hybrid`. |
+| freqai.extrema_weighting.aggregation | `weighted_sum` | enum {`weighted_sum`,`geometric_mean`} | Aggregation method applied to weighted extrema weighting sources for `hybrid`. |
+| freqai.extrema_weighting.aggregation_normalization | `none` | enum {`minmax`,`sigmoid`,`softmax`,`l1`,`l2`,`rank`,`none`} | Normalization method applied to the aggregated extrema weighting source for `hybrid`. |
+| freqai.extrema_weighting.standardization | `none` | enum {`none`,`zscore`,`robust`,`mmad`} | Standardization method applied to weights before normalization. `none`=no standardization, `zscore`=(w-μ)/σ, `robust`=(w-median)/IQR, `mmad`=(w-median)/MAD. |
+| freqai.extrema_weighting.robust_quantiles | [0.25, 0.75] | list[float] where 0 <= Q1 < Q3 <= 1 | Quantile range for robust standardization, Q1 and Q3. |
+| freqai.extrema_weighting.mmad_scaling_factor | 1.4826 | float > 0 | Scaling factor for MMAD standardization. |
+| freqai.extrema_weighting.normalization | `minmax` | enum {`minmax`,`sigmoid`,`softmax`,`l1`,`l2`,`rank`,`none`} | Normalization method applied to weights. |
+| freqai.extrema_weighting.minmax_range | [0.0, 1.0] | list[float] | Target range for `minmax` normalization, min and max. |
+| freqai.extrema_weighting.sigmoid_scale | 1.0 | float > 0 | Scale parameter for `sigmoid` normalization, controls steepness. |
+| freqai.extrema_weighting.softmax_temperature | 1.0 | float > 0 | Temperature parameter for `softmax` normalization: lower values sharpen distribution, higher values flatten it. |
+| freqai.extrema_weighting.rank_method | `average` | enum {`average`,`min`,`max`,`dense`,`ordinal`} | Ranking method for `rank` normalization. |
+| freqai.extrema_weighting.gamma | 1.0 | float (0,10] | Contrast exponent applied 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 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_multiplier | min/max midpoint | float > 0 | Zigzag labeling NATR multiplier. (Deprecated alias: `freqai.feature_parameters.label_natr_ratio`) |
+| freqai.feature_parameters.min_label_natr_multiplier | 9.0 | float > 0 | Minimum labeling NATR multiplier used for reversals labeling HPO. (Deprecated alias: `freqai.feature_parameters.min_label_natr_ratio`) |
+| freqai.feature_parameters.max_label_natr_multiplier | 12.0 | float > 0 | Maximum labeling NATR multiplier used for reversals labeling HPO. (Deprecated alias: `freqai.feature_parameters.max_label_natr_ratio`) |
+| 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 | [1/7,1/7,1/7,1/7,1/7,1/7,1/7] | list[float] | Per-objective weights used in distance calculations to ideal point. 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 | p-order used by `minkowski` / `power_mean` (optional). |
+| freqai.feature_parameters.label_medoid_metric | `euclidean` | string | Metric used with `medoid`. |
+| freqai.feature_parameters.label_kmeans_metric | `euclidean` | string | Metric used for k-means clustering. |
+| freqai.feature_parameters.label_kmeans_selection | `min` | enum {`min`,`medoid`} | Strategy to select trial in the best k-means cluster. |
+| freqai.feature_parameters.label_kmedoids_metric | `euclidean` | string | Metric used for k-medoids clustering. |
+| freqai.feature_parameters.label_kmedoids_selection | `min` | enum {`min`,`medoid`} | Strategy to select trial in the best k-medoids cluster. |
+| freqai.feature_parameters.label_knn_metric | `minkowski` | string | Distance metric for KNN. |
+| freqai.feature_parameters.label_knn_p_order | `None` | float \| None | Tunable for KNN neighbor distances aggregation methods: p-order (`knn_power_mean`, default: 1.0) or quantile (`knn_quantile`, default: 0.5). (optional) |
+| freqai.feature_parameters.label_knn_n_neighbors | 5 | int >= 1 | Number of neighbors for KNN. |
+| _Predictions extrema_ | | | |
+| freqai.predictions_extrema.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.predictions_extrema.threshold_smoothing_method | `mean` | enum {`mean`,`isodata`,`li`,`minimum`,`otsu`,`triangle`,`yen`,`median`,`soft_extremum`} | Thresholding method for prediction thresholds smoothing. (Deprecated alias: `freqai.predictions_extrema.thresholds_smoothing`) |
+| freqai.predictions_extrema.soft_extremum_alpha | 12.0 | float >= 0 | Alpha for `soft_extremum` thresholds smoothing. (Deprecated alias: `freqai.predictions_extrema.thresholds_alpha`) |
+| freqai.predictions_extrema.outlier_threshold_quantile | 0.999 | float (0,1) | Quantile threshold for predictions outlier filtering. (Deprecated alias: `freqai.predictions_extrema.threshold_outlier`) |
+| freqai.predictions_extrema.keep_extrema_fraction | 1.0 | float (0,1] | Fraction of extrema used for thresholds. `1.0` uses all, lower values keep only most significant. Applies to `rank_extrema` and `rank_peaks`; ignored for `partition`. (Deprecated alias: `freqai.predictions_extrema.extrema_fraction`) |
+| _Optuna / HPO_ | | | |
+| freqai.optuna_hyperopt.enabled | false | bool | Enables HPO. |
+| freqai.optuna_hyperopt.sampler | `tpe` | enum {`tpe`,`auto`} | HPO sampler algorithm. `tpe` uses [TPESampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.TPESampler.html) with multivariate and group, `auto` uses [AutoSampler](https://hub.optuna.org/samplers/auto_sampler). |
+| freqai.optuna_hyperopt.storage | `file` | enum {`file`,`sqlite`} | HPO storage backend. |
+| freqai.optuna_hyperopt.continuous | true | bool | Continuous HPO. |
+| freqai.optuna_hyperopt.warm_start | true | bool | Warm start HPO with previous best value(s). |
+| 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 horizon search space. |
+| freqai.optuna_hyperopt.train_candles_step | 10 | int >= 1 | Step for training sets size search space. |
+| freqai.optuna_hyperopt.space_reduction | false | bool | Enable/disable HPO search space reduction based on previous best parameters. |
+| freqai.optuna_hyperopt.space_fraction | 0.4 | float [0,1] | Fraction of the HPO search space to use with `space_reduction`. Lower values create narrower search ranges around the best parameters. (Deprecated alias: `freqai.optuna_hyperopt.expansion_ratio`) |
+| freqai.optuna_hyperopt.min_resource | 3 | int >= 1 | Minimum resource per Hyperband pruner rung. |
+| freqai.optuna_hyperopt.seed | 1 | int >= 0 | HPO RNG seed. |
## ReforceXY
## Purpose
-Research, prototype, and refine advanced ML‑driven and RL‑driven trading strategies for the FreqAI / Freqtrade ecosystem. Two strategies:
-
-- QuickAdapter: ML strategy + adaptive execution heuristics for partial exits, volatility‑aware stop/take profit logic.
-- ReforceXY: RL strategy and reward space analysis.
- Reward space analysis goals:
- - Provide deterministic synthetic sampling and statistical diagnostics to validate reward components and potential‑based shaping behavior and reason about reward parameterization before RL training.
- - Maintain deterministic runs and rich diagnostics to accelerate iteration and anomaly debugging.
+Research, prototype, and refine advanced ML‑driven and RL‑driven trading
+strategies for the FreqAI / Freqtrade ecosystem. Two strategies:
+
+- QuickAdapter: ML strategy + adaptive execution heuristics for partial exits,
+ volatility‑aware stop/take profit logic.
+- ReforceXY: RL strategy and reward space analysis. Reward space analysis goals:
+ - Provide deterministic synthetic sampling and statistical diagnostics to
+ validate reward components and potential‑based shaping behavior and reason
+ about reward parameterization before RL training.
+ - Maintain deterministic runs and rich diagnostics to accelerate iteration and
+ anomaly debugging.
## Tech Stack
### Code Style
-- Base formatting guided by `.editorconfig` (UTF-8, LF, final newline, trimming whitespace, Python indent = 4 spaces, global indent_size=2 for non‑Python where appropriate, max Python line length target 100; Markdown max line length disabled).
+- Base formatting guided by `.editorconfig` (UTF-8, LF, final newline, trimming
+ whitespace, Python indent = 4 spaces, global indent_size=2 for non‑Python
+ where appropriate, max Python line length target 100; Markdown max line length
+ disabled).
- Naming:
- Functions & methods: `snake_case`.
- Constants: `UPPER_SNAKE_CASE`.
- - Internal strategy transient labels/features use prefixes: `"%-"` for engineered feature columns; special markers like `"&s-"` / `"&-"` for internal prediction target(s).
- - Private helpers or internal state use leading underscore (`_exit_thresholds_calibration`).
-- Avoid one‑letter variable names; prefer descriptive one (e.g. `trade_duration_candles`, `natr_ratio_percent`).
-- Prefer explicit type hints (Python 3.11+ built‑in generics: `list[str]`, `dict[str, float]`).
-- Logging: use module logger (`logger = logging.getLogger(__name__)`), info for decision denials, warning for anomalies, error for exceptions.
+ - Internal strategy transient labels/features use prefixes: `"%-"` for
+ engineered feature columns; special markers like `"&s-"` / `"&-"` for
+ internal prediction target(s).
+ - Private helpers or internal state use leading underscore
+ (`_exit_thresholds_calibration`).
+- Avoid one‑letter variable names; prefer descriptive one (e.g.
+ `trade_duration_candles`, `natr_multiplier_fraction`).
+- Prefer explicit type hints (Python 3.11+ built‑in generics: `list[str]`,
+ `dict[str, float]`).
+- Logging: use module logger (`logger = logging.getLogger(__name__)`), info for
+ decision denials, warning for anomalies, error for exceptions.
- No non-English terms in code, docs, comments, logs.
### Architecture Patterns
-- Strategy classes subclass `IStrategy` with model classes subclass `IFreqaiModel`; separate standalone strategy under root directory.
-- Reward Space Analysis: standalone CLI module (`reward_space_analysis.py`) + tests focusing on deterministic synthetic scenario generation, decomposition, statistical validation, potential‑based reward shaping (PBRS) variants.
-- Separation of concerns: reward analytic tooling does not depend on strategy runtime state; consumption is offline pre‑training / validation.
+- Strategy classes subclass `IStrategy` with model classes subclass
+ `IFreqaiModel`; separate standalone strategy under root directory.
+- Reward Space Analysis: standalone CLI module (`reward_space_analysis.py`) +
+ tests focusing on deterministic synthetic scenario generation, decomposition,
+ statistical validation, potential‑based reward shaping (PBRS) variants.
+- Separation of concerns: reward analytic tooling does not depend on strategy
+ runtime state; consumption is offline pre‑training / validation.
### Reward Space Analysis Testing Strategy
- PyTest test modules in `reward_space_analysis/tests/<focus>`.
-- Focus: correctness of reward calculations, statistical invariants, PBRS modes, transforms, robustness, integration end‑to‑end.
-- Logging configured for concise INFO output; colored, warnings disabled by default in test runs.
-- Coverage goal: ≥85% on new analytical logic; critical reward shaping paths must be exercised (component bounds, invariance checks, exit attenuation kernels, transform functions, distribution metrics).
-- Focused test invocation examples (integration, statistical coherence, reward alignment) documented in README.
-- Run tests after: modifying reward logic; before major analyses; dependency or Python version changes; unexpected anomalies.
+- Focus: correctness of reward calculations, statistical invariants, PBRS modes,
+ transforms, robustness, integration end‑to‑end.
+- Logging configured for concise INFO output; colored, warnings disabled by
+ default in test runs.
+- Coverage goal: ≥85% on new analytical logic; critical reward shaping paths
+ must be exercised (component bounds, invariance checks, exit attenuation
+ kernels, transform functions, distribution metrics).
+- Focused test invocation examples (integration, statistical coherence, reward
+ alignment) documented in README.
+- Run tests after: modifying reward logic; before major analyses; dependency or
+ Python version changes; unexpected anomalies.
### Git Workflow
-- Primary branch: `main`. Feature / experiment branches should be: `feat/<concise-topic>`, `exp/<strategy-or-reward-param>`. Fix branches should be: `fix/<bug>`.
-- Commit messages: imperative, follow Conventional Commits. Emphasize WHY over raw WHAT when non‑obvious.
-- Avoid large mixed commits; isolate analytical tooling changes from strategy behavior changes.
-- Keep manifests and generated outputs out of version control (only code + templates); user data directories contain `.gitkeep` placeholders.
+- Primary branch: `main`. Feature / experiment branches should be:
+ `feat/<concise-topic>`, `exp/<strategy-or-reward-param>`. Fix branches should
+ be: `fix/<bug>`.
+- Commit messages: imperative, follow Conventional Commits. Emphasize WHY over
+ raw WHAT when non‑obvious.
+- Avoid large mixed commits; isolate analytical tooling changes from strategy
+ behavior changes.
+- Keep manifests and generated outputs out of version control (only code +
+ templates); user data directories contain `.gitkeep` placeholders.
## Domain Context
- Strategies operate on sequential market OHLCV data.
-- QuickAdapterV3 features engineered include volatility metrics (NATR/ATR), momentum (MACD, EWO), market structure shift (extrema labeling via zigzag), band widths (BB, KC, VWAP), and price distance measures.
-- QuickAdapterV3 integrates dynamic volatility interpolation (weighted/moving average/interpolation modes) to derive adaptive NATR for stoploss/take profit calculations; partial exits based on staged NATR ratio percentages.
-- ReforceXY reward shaping emphasizes potential‑based reward shaping (PBRS) invariance: canonical vs non canonical modes, hold/entry/exit additive toggles, duration penalties, exit attenuation kernels (linear/power/half‑life/etc.).
+- QuickAdapterV3 features engineered include volatility metrics (NATR/ATR),
+ momentum (MACD, EWO), market structure shift (extrema labeling via zigzag),
+ band widths (BB, KC, VWAP), and price distance measures.
+- QuickAdapterV3 integrates dynamic volatility interpolation (weighted/moving
+ average/interpolation modes) to derive adaptive NATR for stoploss/take profit
+ calculations; partial exits based on staged NATR ratio percentages.
+- ReforceXY reward shaping emphasizes potential‑based reward shaping (PBRS)
+ invariance: canonical vs non canonical modes, hold/entry/exit additive
+ toggles, duration penalties, exit attenuation kernels
+ (linear/power/half‑life/etc.).
## Important Constraints
- Python version ≥3.11 (target for type hints).
-- Trading mode affects short availability (spot disallows shorts); logic must gate short entries accordingly.
+- Trading mode affects short availability (spot disallows shorts); logic must
+ gate short entries accordingly.
- Computations must handle missing/NaN gracefully.
-- Regulatory / business: none explicit; treat strategies as experimental research (no performance guarantees) and avoid embedding sensitive credentials.
+- Regulatory / business: none explicit; treat strategies as experimental
+ research (no performance guarantees) and avoid embedding sensitive
+ credentials.
## External Dependencies
- Freqtrade / FreqAI framework APIs.
-- Docker images defined per strategy project (`Dockerfile.quickadapter`, `Dockerfile.reforcexy`) for containerized execution.
+- Docker images defined per strategy project (`Dockerfile.quickadapter`,
+ `Dockerfile.reforcexy`) for containerized execution.
"price_last_balance": 0.0
},
"reversal_confirmation": {
- "max_natr_ratio_percent": 0.05
+ "max_natr_multiplier_fraction": 0.05
},
"exchange": {
"name": "binance",
},
"extrema_smoothing": {
"method": "kaiser",
- "window": 5,
+ "window_candles": 5,
"beta": 12.0
},
"predictions_extrema": {
- "thresholds_smoothing": "isodata"
+ "threshold_smoothing_method": "isodata"
},
"optuna_hyperopt": {
"enabled": true,
"&s-minima_threshold": -2,
"&s-maxima_threshold": 2,
"label_period_candles": 18,
- "label_natr_ratio": 10.5,
+ "label_natr_multiplier": 10.5,
"hp_rmse": -1,
"train_rmse": -1
},
"4h"
],
"label_period_candles": 18,
+ "label_natr_multiplier": 10.5,
"label_metric": "kmedoids",
"include_shifted_candles": 6,
"DI_threshold": 10,
calculate_n_extrema,
fit_regressor,
format_number,
+ get_config_value,
get_label_defaults,
get_min_max_label_period_candles,
get_optuna_callbacks,
https://github.com/sponsors/robcaulk
"""
- version = "3.7.141"
+ version = "3.8.0"
_TEST_SIZE: Final[float] = 0.1
"jensenshannon",
)
- PREDICTIONS_EXTREMA_THRESHOLD_OUTLIER_DEFAULT: Final[float] = 0.999
- PREDICTIONS_EXTREMA_THRESHOLDS_ALPHA_DEFAULT: Final[float] = 12.0
- PREDICTIONS_EXTREMA_EXTREMA_FRACTION_DEFAULT: Final[float] = 1.0
+ PREDICTIONS_EXTREMA_OUTLIER_THRESHOLD_QUANTILE_DEFAULT: Final[float] = 0.999
+ PREDICTIONS_EXTREMA_SOFT_EXTREMUM_ALPHA_DEFAULT: Final[float] = 12.0
+ PREDICTIONS_EXTREMA_KEEP_EXTREMA_FRACTION_DEFAULT: Final[float] = 1.0
FIT_LIVE_PREDICTIONS_CANDLES_DEFAULT: Final[int] = (
DEFAULT_FIT_LIVE_PREDICTIONS_CANDLES
)
MIN_LABEL_PERIOD_CANDLES_DEFAULT: Final[int] = 12
MAX_LABEL_PERIOD_CANDLES_DEFAULT: Final[int] = 24
- MIN_LABEL_NATR_RATIO_DEFAULT: Final[float] = 9.0
- MAX_LABEL_NATR_RATIO_DEFAULT: Final[float] = 12.0
+ MIN_LABEL_NATR_MULTIPLIER_DEFAULT: Final[float] = 9.0
+ MAX_LABEL_NATR_MULTIPLIER_DEFAULT: Final[float] = 12.0
LABEL_KNN_N_NEIGHBORS_DEFAULT: Final[int] = 5
@staticmethod
"label_candles_step": 1,
"train_candles_step": 10,
"space_reduction": False,
- "expansion_ratio": 0.4,
+ "space_fraction": 0.4,
"min_resource": 3,
"seed": 1,
}
+ optuna_hyperopt = self.config.get("freqai", {}).get("optuna_hyperopt", {})
+ get_config_value(
+ optuna_hyperopt,
+ new_key="space_fraction",
+ old_key="expansion_ratio",
+ default=optuna_default_config["space_fraction"],
+ logger=logger,
+ new_path="freqai.optuna_hyperopt.space_fraction",
+ old_path="freqai.optuna_hyperopt.expansion_ratio",
+ )
return {
**optuna_default_config,
- **self.config.get("freqai", {}).get("optuna_hyperopt", {}),
+ **optuna_hyperopt,
}
+ @cached_property
+ def _min_label_period_candles(self) -> int:
+ return self.ft_params.get(
+ "min_label_period_candles",
+ QuickAdapterRegressorV3.MIN_LABEL_PERIOD_CANDLES_DEFAULT,
+ )
+
+ @cached_property
+ def _max_label_period_candles(self) -> int:
+ return self.ft_params.get(
+ "max_label_period_candles",
+ QuickAdapterRegressorV3.MAX_LABEL_PERIOD_CANDLES_DEFAULT,
+ )
+
+ @cached_property
+ def _min_label_natr_multiplier(self) -> float:
+ return self.ft_params.get(
+ "min_label_natr_multiplier",
+ QuickAdapterRegressorV3.MIN_LABEL_NATR_MULTIPLIER_DEFAULT,
+ )
+
+ @cached_property
+ def _max_label_natr_multiplier(self) -> float:
+ return self.ft_params.get(
+ "max_label_natr_multiplier",
+ QuickAdapterRegressorV3.MAX_LABEL_NATR_MULTIPLIER_DEFAULT,
+ )
+
@cached_property
def _label_frequency_candles(self) -> int:
"""
if not isinstance(predictions_extrema, dict):
predictions_extrema = {}
- threshold_outlier = predictions_extrema.get(
- "threshold_outlier",
- QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_THRESHOLD_OUTLIER_DEFAULT,
+ outlier_threshold_quantile = get_config_value(
+ predictions_extrema,
+ new_key="outlier_threshold_quantile",
+ old_key="threshold_outlier",
+ default=QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_OUTLIER_THRESHOLD_QUANTILE_DEFAULT,
+ logger=logger,
+ new_path="freqai.predictions_extrema.outlier_threshold_quantile",
+ old_path="freqai.predictions_extrema.threshold_outlier",
)
if (
- not isinstance(threshold_outlier, (int, float))
- or not np.isfinite(threshold_outlier)
- or not (0 < threshold_outlier < 1)
+ not isinstance(outlier_threshold_quantile, (int, float))
+ or not np.isfinite(outlier_threshold_quantile)
+ or not (0 < outlier_threshold_quantile < 1)
):
- threshold_outlier = (
- QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_THRESHOLD_OUTLIER_DEFAULT
- )
+ outlier_threshold_quantile = QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_OUTLIER_THRESHOLD_QUANTILE_DEFAULT
selection_method = str(
predictions_extrema.get(
):
selection_method = QuickAdapterRegressorV3._EXTREMA_SELECTION_METHODS[0]
- thresholds_smoothing = str(
- predictions_extrema.get(
- "thresholds_smoothing",
- QuickAdapterRegressorV3._THRESHOLD_METHODS[0], # "mean"
+ threshold_smoothing_method = str(
+ get_config_value(
+ predictions_extrema,
+ new_key="threshold_smoothing_method",
+ old_key="thresholds_smoothing",
+ default=QuickAdapterRegressorV3._THRESHOLD_METHODS[0], # "mean"
+ logger=logger,
+ new_path="freqai.predictions_extrema.threshold_smoothing_method",
+ old_path="freqai.predictions_extrema.thresholds_smoothing",
)
)
- if thresholds_smoothing not in QuickAdapterRegressorV3._threshold_methods_set():
- thresholds_smoothing = QuickAdapterRegressorV3._THRESHOLD_METHODS[
+ if (
+ threshold_smoothing_method
+ not in QuickAdapterRegressorV3._threshold_methods_set()
+ ):
+ threshold_smoothing_method = QuickAdapterRegressorV3._THRESHOLD_METHODS[
0
] # "mean"
- thresholds_alpha = predictions_extrema.get(
- "thresholds_alpha",
- QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_THRESHOLDS_ALPHA_DEFAULT,
+ soft_extremum_alpha = get_config_value(
+ predictions_extrema,
+ new_key="soft_extremum_alpha",
+ old_key="thresholds_alpha",
+ default=QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_SOFT_EXTREMUM_ALPHA_DEFAULT,
+ logger=logger,
+ new_path="freqai.predictions_extrema.soft_extremum_alpha",
+ old_path="freqai.predictions_extrema.thresholds_alpha",
)
if (
- not isinstance(thresholds_alpha, (int, float))
- or not np.isfinite(thresholds_alpha)
- or thresholds_alpha < 0
+ not isinstance(soft_extremum_alpha, (int, float))
+ or not np.isfinite(soft_extremum_alpha)
+ or soft_extremum_alpha < 0
):
- thresholds_alpha = (
- QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_THRESHOLDS_ALPHA_DEFAULT
- )
-
- extrema_fraction = predictions_extrema.get(
- "extrema_fraction",
- QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_EXTREMA_FRACTION_DEFAULT,
+ soft_extremum_alpha = (
+ QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_SOFT_EXTREMUM_ALPHA_DEFAULT
+ )
+
+ keep_extrema_fraction = get_config_value(
+ predictions_extrema,
+ new_key="keep_extrema_fraction",
+ old_key="extrema_fraction",
+ default=QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_KEEP_EXTREMA_FRACTION_DEFAULT,
+ logger=logger,
+ new_path="freqai.predictions_extrema.keep_extrema_fraction",
+ old_path="freqai.predictions_extrema.extrema_fraction",
)
- if not isinstance(extrema_fraction, (int, float)) or not (
- 0 < extrema_fraction <= 1
+ if not isinstance(keep_extrema_fraction, (int, float)) or not (
+ 0 < keep_extrema_fraction <= 1
):
- extrema_fraction = (
- QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_EXTREMA_FRACTION_DEFAULT
- )
+ keep_extrema_fraction = QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_KEEP_EXTREMA_FRACTION_DEFAULT
return {
- "threshold_outlier": float(threshold_outlier),
+ "outlier_threshold_quantile": float(outlier_threshold_quantile),
"selection_method": selection_method,
- "thresholds_smoothing": thresholds_smoothing,
- "thresholds_alpha": float(thresholds_alpha),
- "extrema_fraction": float(extrema_fraction),
+ "threshold_smoothing_method": threshold_smoothing_method,
+ "soft_extremum_alpha": float(soft_extremum_alpha),
+ "keep_extrema_fraction": float(keep_extrema_fraction),
}
@property
self._optuna_label_candle: dict[str, int] = {}
self._optuna_label_candles: dict[str, int] = {}
self._optuna_label_incremented_pairs: list[str] = []
- self._default_label_natr_ratio, self._default_label_period_candles = (
+ self._default_label_natr_multiplier, self._default_label_period_candles = (
get_label_defaults(self.ft_params, logger)
)
for pair in self.pairs:
"label_period_candles",
self._default_label_period_candles,
),
- "label_natr_ratio": float(
+ "label_natr_multiplier": float(
self.ft_params.get(
- "label_natr_ratio",
- self._default_label_natr_ratio,
+ "label_natr_multiplier",
+ self._default_label_natr_multiplier,
)
),
}
)
logger.info(f" space_reduction: {optuna_config.get('space_reduction')}")
logger.info(
- f" expansion_ratio: {format_number(optuna_config.get('expansion_ratio'))}"
+ f" space_fraction: {format_number(optuna_config.get('space_fraction'))}"
)
logger.info(f" min_resource: {optuna_config.get('min_resource')}")
logger.info(f" seed: {optuna_config.get('seed')}")
f" selection_method: {predictions_extrema.get('selection_method')}"
)
logger.info(
- f" thresholds_smoothing: {predictions_extrema.get('thresholds_smoothing')}"
+ f" threshold_smoothing_method: {predictions_extrema.get('threshold_smoothing_method')}"
)
logger.info(
- f" threshold_outlier: {format_number(predictions_extrema.get('threshold_outlier'))}"
+ f" outlier_threshold_quantile: {format_number(predictions_extrema.get('outlier_threshold_quantile'))}"
)
logger.info(
- f" thresholds_alpha: {format_number(predictions_extrema.get('thresholds_alpha'))}"
+ f" soft_extremum_alpha: {format_number(predictions_extrema.get('soft_extremum_alpha'))}"
)
logger.info(
- f" extrema_fraction: {format_number(predictions_extrema.get('extrema_fraction'))}"
+ f" keep_extrema_fraction: {format_number(predictions_extrema.get('keep_extrema_fraction'))}"
)
logger.info("Label Configuration:")
f" fit_live_predictions_candles: {self.freqai_info.get('fit_live_predictions_candles', QuickAdapterRegressorV3.FIT_LIVE_PREDICTIONS_CANDLES_DEFAULT)}"
)
logger.info(f" label_frequency_candles: {self._label_frequency_candles}")
+ logger.info(f" min_label_period_candles: {self._min_label_period_candles}")
+ logger.info(f" max_label_period_candles: {self._max_label_period_candles}")
logger.info(
- f" min_label_period_candles: {self.ft_params.get('min_label_period_candles', QuickAdapterRegressorV3.MIN_LABEL_PERIOD_CANDLES_DEFAULT)}"
- )
- logger.info(
- f" max_label_period_candles: {self.ft_params.get('max_label_period_candles', QuickAdapterRegressorV3.MAX_LABEL_PERIOD_CANDLES_DEFAULT)}"
- )
- logger.info(
- f" min_label_natr_ratio: {format_number(self.ft_params.get('min_label_natr_ratio', QuickAdapterRegressorV3.MIN_LABEL_NATR_RATIO_DEFAULT))}"
+ f" min_label_natr_multiplier: {format_number(self._min_label_natr_multiplier)}"
)
logger.info(
- f" max_label_natr_ratio: {format_number(self.ft_params.get('max_label_natr_ratio', QuickAdapterRegressorV3.MAX_LABEL_NATR_RATIO_DEFAULT))}"
+ f" max_label_natr_multiplier: {format_number(self._max_label_natr_multiplier)}"
)
if self._optuna_hyperopt:
if params:
logger.info(
f" {pair}: label_period_candles={params.get('label_period_candles')}, "
- f"label_natr_ratio={format_number(params.get('label_natr_ratio'))}"
+ f"label_natr_multiplier={format_number(params.get('label_natr_multiplier'))}"
)
else:
logger.info("Label Parameters:")
f" label_period_candles: {self.ft_params.get('label_period_candles', self._default_label_period_candles)}"
)
logger.info(
- f" label_natr_ratio: {format_number(float(self.ft_params.get('label_natr_ratio', self._default_label_natr_ratio)))}"
+ f" label_natr_multiplier: {format_number(float(self.ft_params.get('label_natr_multiplier', self._default_label_natr_multiplier)))}"
)
logger.info("=" * 60)
), # "hp"
model_training_parameters,
self._optuna_config.get("space_reduction"),
- self._optuna_config.get("expansion_ratio"),
+ self._optuna_config.get("space_fraction"),
),
direction=optuna.study.StudyDirection.MINIMIZE,
)
),
fit_live_predictions_candles,
self._optuna_config.get("label_candles_step"),
- min_label_period_candles=self.ft_params.get(
- "min_label_period_candles",
- QuickAdapterRegressorV3.MIN_LABEL_PERIOD_CANDLES_DEFAULT,
- ),
- max_label_period_candles=self.ft_params.get(
- "max_label_period_candles",
- QuickAdapterRegressorV3.MAX_LABEL_PERIOD_CANDLES_DEFAULT,
- ),
- min_label_natr_ratio=self.ft_params.get(
- "min_label_natr_ratio",
- QuickAdapterRegressorV3.MIN_LABEL_NATR_RATIO_DEFAULT,
- ),
- max_label_natr_ratio=self.ft_params.get(
- "max_label_natr_ratio",
- QuickAdapterRegressorV3.MAX_LABEL_NATR_RATIO_DEFAULT,
- ),
+ min_label_period_candles=self._min_label_period_candles,
+ max_label_period_candles=self._max_label_period_candles,
+ min_label_natr_multiplier=self._min_label_natr_multiplier,
+ max_label_natr_multiplier=self._max_label_natr_multiplier,
),
directions=list(QuickAdapterRegressorV3._OPTUNA_LABEL_DIRECTIONS),
),
pd.to_numeric(di_values, errors="coerce").dropna(), floc=0
)
cutoff = sp.stats.weibull_min.ppf(
- self.predictions_extrema["threshold_outlier"], *f
+ self.predictions_extrema["outlier_threshold_quantile"], *f
)
dk.data["DI_value_mean"] = di_values.mean()
pair, QuickAdapterRegressorV3._OPTUNA_NAMESPACES[2]
).get("label_period_candles") # "label"
)
- dk.data["extra_returns_per_train"]["label_natr_ratio"] = self.get_optuna_params(
- pair,
- QuickAdapterRegressorV3._OPTUNA_NAMESPACES[2], # "label"
- ).get("label_natr_ratio")
+ dk.data["extra_returns_per_train"]["label_natr_multiplier"] = (
+ self.get_optuna_params(
+ pair,
+ QuickAdapterRegressorV3._OPTUNA_NAMESPACES[2], # "label"
+ ).get("label_natr_multiplier")
+ )
hp_rmse = self.optuna_validate_value(
self.get_optuna_value(pair, QuickAdapterRegressorV3._OPTUNA_NAMESPACES[0])
pred_extrema = pred_df.get(EXTREMA_COLUMN).iloc[-thresholds_candles:].copy()
extrema_selection = self.predictions_extrema["selection_method"]
- thresholds_smoothing = self.predictions_extrema["thresholds_smoothing"]
- extrema_fraction = self.predictions_extrema["extrema_fraction"]
+ threshold_smoothing_method = self.predictions_extrema[
+ "threshold_smoothing_method"
+ ]
+ keep_extrema_fraction = self.predictions_extrema["keep_extrema_fraction"]
if (
- thresholds_smoothing == QuickAdapterRegressorV3._THRESHOLD_METHODS[7]
+ threshold_smoothing_method == QuickAdapterRegressorV3._THRESHOLD_METHODS[7]
): # "median"
return QuickAdapterRegressorV3.median_min_max(
- pred_extrema, extrema_selection, extrema_fraction
+ pred_extrema, extrema_selection, keep_extrema_fraction
)
elif (
- thresholds_smoothing == QuickAdapterRegressorV3._THRESHOLD_METHODS[8]
+ threshold_smoothing_method == QuickAdapterRegressorV3._THRESHOLD_METHODS[8]
): # "soft_extremum"
return QuickAdapterRegressorV3.soft_extremum_min_max(
pred_extrema,
- self.predictions_extrema["thresholds_alpha"],
+ self.predictions_extrema["soft_extremum_alpha"],
extrema_selection,
- extrema_fraction,
+ keep_extrema_fraction,
)
elif (
- thresholds_smoothing
+ threshold_smoothing_method
in QuickAdapterRegressorV3._skimage_threshold_methods_set()
):
return QuickAdapterRegressorV3.skimage_min_max(
- pred_extrema, thresholds_smoothing, extrema_selection, extrema_fraction
+ pred_extrema,
+ threshold_smoothing_method,
+ extrema_selection,
+ keep_extrema_fraction,
)
@staticmethod
pred_extrema: pd.Series,
minima_indices: NDArray[np.intp],
maxima_indices: NDArray[np.intp],
- extrema_fraction: float = 1.0,
+ keep_extrema_fraction: float = 1.0,
) -> tuple[pd.Series, pd.Series]:
n_minima = (
- max(1, int(round(minima_indices.size * extrema_fraction)))
+ max(1, int(round(minima_indices.size * keep_extrema_fraction)))
if minima_indices.size > 0
else 0
)
n_maxima = (
- max(1, int(round(maxima_indices.size * extrema_fraction)))
+ max(1, int(round(maxima_indices.size * keep_extrema_fraction)))
if maxima_indices.size > 0
else 0
)
pred_extrema: pd.Series,
n_minima: int,
n_maxima: int,
- extrema_fraction: float = 1.0,
+ keep_extrema_fraction: float = 1.0,
) -> tuple[pd.Series, pd.Series]:
pred_minima = (
- pred_extrema.nsmallest(max(1, int(round(n_minima * extrema_fraction))))
+ pred_extrema.nsmallest(max(1, int(round(n_minima * keep_extrema_fraction))))
if n_minima > 0
else pd.Series(dtype=float)
)
pred_maxima = (
- pred_extrema.nlargest(max(1, int(round(n_maxima * extrema_fraction))))
+ pred_extrema.nlargest(max(1, int(round(n_maxima * keep_extrema_fraction))))
if n_maxima > 0
else pd.Series(dtype=float)
)
def get_pred_min_max(
pred_extrema: pd.Series,
extrema_selection: ExtremaSelectionMethod,
- extrema_fraction: float = 1.0,
+ keep_extrema_fraction: float = 1.0,
) -> tuple[pd.Series, pd.Series]:
pred_extrema = (
pd.to_numeric(pred_extrema, errors="coerce")
pred_extrema,
minima_indices.size,
maxima_indices.size,
- extrema_fraction,
+ keep_extrema_fraction,
)
elif (
QuickAdapterRegressorV3._get_extrema_indices(pred_extrema)
)
pred_minima, pred_maxima = QuickAdapterRegressorV3._get_ranked_peaks(
- pred_extrema, minima_indices, maxima_indices, extrema_fraction
+ pred_extrema, minima_indices, maxima_indices, keep_extrema_fraction
)
elif (
pred_extrema: pd.Series,
alpha: float,
extrema_selection: ExtremaSelectionMethod,
- extrema_fraction: float = 1.0,
+ keep_extrema_fraction: float = 1.0,
) -> tuple[float, float]:
if alpha < 0:
raise ValueError(f"Invalid alpha {alpha!r}: must be >= 0")
pred_minima, pred_maxima = QuickAdapterRegressorV3.get_pred_min_max(
- pred_extrema, extrema_selection, extrema_fraction
+ pred_extrema, extrema_selection, keep_extrema_fraction
)
soft_minimum = soft_extremum(pred_minima, alpha=-alpha)
if not np.isfinite(soft_minimum):
def median_min_max(
pred_extrema: pd.Series,
extrema_selection: ExtremaSelectionMethod,
- extrema_fraction: float = 1.0,
+ keep_extrema_fraction: float = 1.0,
) -> tuple[float, float]:
pred_minima, pred_maxima = QuickAdapterRegressorV3.get_pred_min_max(
- pred_extrema, extrema_selection, extrema_fraction
+ pred_extrema, extrema_selection, keep_extrema_fraction
)
if pred_minima.empty:
pred_extrema: pd.Series,
method: str,
extrema_selection: ExtremaSelectionMethod,
- extrema_fraction: float = 1.0,
+ keep_extrema_fraction: float = 1.0,
) -> tuple[float, float]:
pred_minima, pred_maxima = QuickAdapterRegressorV3.get_pred_min_max(
- pred_extrema, extrema_selection, extrema_fraction
+ pred_extrema, extrema_selection, keep_extrema_fraction
)
try:
model_training_best_parameters: dict[str, Any],
model_training_parameters: dict[str, Any],
space_reduction: bool,
- expansion_ratio: float,
+ space_fraction: float,
) -> float:
study_model_parameters = get_optuna_study_model_parameters(
trial,
regressor,
model_training_best_parameters,
space_reduction,
- expansion_ratio,
+ space_fraction,
)
model_training_parameters = {**model_training_parameters, **study_model_parameters}
candles_step: int,
min_label_period_candles: int = QuickAdapterRegressorV3.MIN_LABEL_PERIOD_CANDLES_DEFAULT,
max_label_period_candles: int = QuickAdapterRegressorV3.MAX_LABEL_PERIOD_CANDLES_DEFAULT,
- min_label_natr_ratio: float = QuickAdapterRegressorV3.MIN_LABEL_NATR_RATIO_DEFAULT,
- max_label_natr_ratio: float = QuickAdapterRegressorV3.MAX_LABEL_NATR_RATIO_DEFAULT,
+ min_label_natr_multiplier: float = QuickAdapterRegressorV3.MIN_LABEL_NATR_MULTIPLIER_DEFAULT,
+ max_label_natr_multiplier: float = QuickAdapterRegressorV3.MAX_LABEL_NATR_MULTIPLIER_DEFAULT,
) -> tuple[int, float, float, float, float, float, float]:
min_label_period_candles, max_label_period_candles, candles_step = (
get_min_max_label_period_candles(
max_label_period_candles,
step=candles_step,
)
- label_natr_ratio = trial.suggest_float(
- "label_natr_ratio", min_label_natr_ratio, max_label_natr_ratio, step=0.05
+ label_natr_multiplier = trial.suggest_float(
+ "label_natr_multiplier",
+ min_label_natr_multiplier,
+ max_label_natr_multiplier,
+ step=0.05,
)
df = df.iloc[
) = zigzag(
df,
natr_period=label_period_candles,
- natr_ratio=label_natr_ratio,
+ natr_multiplier=label_natr_multiplier,
)
median_amplitude = np.nanmedian(np.asarray(pivots_amplitudes, dtype=float))
ewo,
format_number,
get_callable_sha256,
+ get_config_value,
get_distance,
get_label_defaults,
get_weighted_extrema,
_TRADING_MODES: Final[tuple[TradingMode, ...]] = ("spot", "margin", "futures")
def version(self) -> str:
- return "3.3.191"
+ return "3.8.0"
timeframe = "5m"
}
default_reversal_confirmation: ClassVar[dict[str, int | float]] = {
- "lookback_period": 0,
- "decay_ratio": 0.5,
- "min_natr_ratio_percent": 0.0095,
- "max_natr_ratio_percent": 0.075,
+ "lookback_period_candles": 0,
+ "decay_fraction": 0.5,
+ "min_natr_multiplier_fraction": 0.0095,
+ "max_natr_multiplier_fraction": 0.075,
}
position_adjustment_enable = True
- # {stage: (natr_ratio_percent, stake_percent, color)}
+ # {stage: (natr_multiplier_fraction, stake_percent, color)}
partial_exit_stages: ClassVar[dict[int, tuple[float, float, str]]] = {
0: (0.4858, 0.4, "lime"),
1: (0.6180, 0.3, "yellow"),
2: (0.7640, 0.2, "coral"),
}
- # (natr_ratio_percent, stake_percent, color)
+ # (natr_multiplier_fraction, stake_percent, color)
_FINAL_EXIT_STAGE: Final[tuple[float, float, str]] = (1.0, 1.0, "deepskyblue")
- _CUSTOM_STOPLOSS_NATR_RATIO_PERCENT: Final[float] = 0.7860
+ _CUSTOM_STOPLOSS_NATR_MULTIPLIER_FRACTION: Final[float] = 0.7860
_ANNOTATION_LINE_OFFSET_CANDLES: Final[int] = 10
/ self.freqai_info.get("identifier")
)
feature_parameters = self.freqai_info.get("feature_parameters", {})
- self._default_label_natr_ratio, self._default_label_period_candles = (
+ self._default_label_natr_multiplier, self._default_label_period_candles = (
get_label_defaults(feature_parameters, logger)
)
self._label_params: dict[str, dict[str, Any]] = {}
"label_period_candles",
self._default_label_period_candles,
),
- "label_natr_ratio": float(
+ "label_natr_multiplier": float(
feature_parameters.get(
- "label_natr_ratio",
- self._default_label_natr_ratio,
+ "label_natr_multiplier",
+ self._default_label_natr_multiplier,
)
),
}
logger.info("Extrema Smoothing:")
logger.info(f" method: {self.extrema_smoothing['method']}")
- logger.info(f" window: {self.extrema_smoothing['window']}")
+ logger.info(f" window_candles: {self.extrema_smoothing['window_candles']}")
logger.info(f" beta: {format_number(self.extrema_smoothing['beta'])}")
logger.info(f" polyorder: {self.extrema_smoothing['polyorder']}")
logger.info(f" mode: {self.extrema_smoothing['mode']}")
logger.info(f" sigma: {format_number(self.extrema_smoothing['sigma'])}")
logger.info("Reversal Confirmation:")
- logger.info(f" lookback_period: {self._reversal_lookback_period}")
- logger.info(f" decay_ratio: {format_number(self._reversal_decay_ratio)}")
logger.info(
- f" min_natr_ratio_percent: {format_number(self._reversal_min_natr_ratio_percent)}"
+ f" lookback_period_candles: {self._reversal_lookback_period_candles}"
)
+ logger.info(f" decay_fraction: {format_number(self._reversal_decay_fraction)}")
logger.info(
- f" max_natr_ratio_percent: {format_number(self._reversal_max_natr_ratio_percent)}"
+ f" min_natr_multiplier_fraction: {format_number(self._reversal_min_natr_multiplier_fraction)}"
+ )
+ logger.info(
+ f" max_natr_multiplier_fraction: {format_number(self._reversal_max_natr_multiplier_fraction)}"
)
exit_pricing = self.config.get("exit_pricing", {})
- trade_price_target = exit_pricing.get("trade_price_target", "moving_average")
+ trade_price_target_method = get_config_value(
+ exit_pricing,
+ new_key="trade_price_target_method",
+ old_key="trade_price_target",
+ default=TRADE_PRICE_TARGETS[0], # "moving_average"
+ logger=logger,
+ new_path="exit_pricing.trade_price_target_method",
+ old_path="exit_pricing.trade_price_target",
+ )
logger.info("Exit Pricing:")
- logger.info(f" trade_price_target: {trade_price_target}")
+ logger.info(f" trade_price_target_method: {trade_price_target_method}")
logger.info(f" thresholds_calibration: {self._exit_thresholds_calibration}")
logger.info("Custom Stoploss:")
logger.info(
- f" natr_ratio_percent: {format_number(QuickAdapterV3._CUSTOM_STOPLOSS_NATR_RATIO_PERCENT)}"
+ f" natr_multiplier_fraction: {format_number(QuickAdapterV3._CUSTOM_STOPLOSS_NATR_MULTIPLIER_FRACTION)}"
)
logger.info("Partial Exit Stages:")
for stage, (
- natr_ratio_percent,
+ natr_multiplier_fraction,
stake_percent,
color,
) in QuickAdapterV3.partial_exit_stages.items():
logger.info(
- f" stage {stage}: natr_ratio_percent={format_number(natr_ratio_percent)}, stake_percent={format_number(stake_percent)}, color={color}"
+ f" stage {stage}: natr_multiplier_fraction={format_number(natr_multiplier_fraction)}, stake_percent={format_number(stake_percent)}, color={color}"
)
final_stage = max(QuickAdapterV3.partial_exit_stages.keys(), default=-1) + 1
logger.info(
- f"Final Exit Stage: stage {final_stage}: natr_ratio_percent={format_number(QuickAdapterV3._FINAL_EXIT_STAGE[0])}, stake_percent={format_number(QuickAdapterV3._FINAL_EXIT_STAGE[1])}, color={QuickAdapterV3._FINAL_EXIT_STAGE[2]}"
+ f"Final Exit Stage: stage {final_stage}: natr_multiplier_fraction={format_number(QuickAdapterV3._FINAL_EXIT_STAGE[0])}, stake_percent={format_number(QuickAdapterV3._FINAL_EXIT_STAGE[1])}, color={QuickAdapterV3._FINAL_EXIT_STAGE[2]}"
)
logger.info("Protections:")
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"],
+ lookback_period_candles = get_config_value(
+ reversal_confirmation,
+ new_key="lookback_period_candles",
+ old_key="lookback_period",
+ default=QuickAdapterV3.default_reversal_confirmation[
+ "lookback_period_candles"
+ ],
+ logger=logger,
+ new_path="reversal_confirmation.lookback_period_candles",
+ old_path="reversal_confirmation.lookback_period",
+ )
+ decay_fraction = get_config_value(
+ reversal_confirmation,
+ new_key="decay_fraction",
+ old_key="decay_ratio",
+ default=QuickAdapterV3.default_reversal_confirmation["decay_fraction"],
+ logger=logger,
+ new_path="reversal_confirmation.decay_fraction",
+ old_path="reversal_confirmation.decay_ratio",
+ )
+
+ min_natr_multiplier_fraction = get_config_value(
+ reversal_confirmation,
+ new_key="min_natr_multiplier_fraction",
+ old_key="min_natr_ratio_percent",
+ default=QuickAdapterV3.default_reversal_confirmation[
+ "min_natr_multiplier_fraction"
+ ],
+ logger=logger,
+ new_path="reversal_confirmation.min_natr_multiplier_fraction",
+ old_path="reversal_confirmation.min_natr_ratio_percent",
+ )
+ max_natr_multiplier_fraction = get_config_value(
+ reversal_confirmation,
+ new_key="max_natr_multiplier_fraction",
+ old_key="max_natr_ratio_percent",
+ default=QuickAdapterV3.default_reversal_confirmation[
+ "max_natr_multiplier_fraction"
+ ],
+ logger=logger,
+ new_path="reversal_confirmation.max_natr_multiplier_fraction",
+ old_path="reversal_confirmation.max_natr_ratio_percent",
)
- if not isinstance(lookback_period, int) or lookback_period < 0:
+ if not isinstance(lookback_period_candles, int) or lookback_period_candles < 0:
logger.warning(
- f"Invalid reversal_confirmation lookback_period {lookback_period!r}: must be >= 0. Using default {QuickAdapterV3.default_reversal_confirmation['lookback_period']!r}"
+ f"Invalid reversal_confirmation lookback_period_candles {lookback_period_candles!r}: must be >= 0. Using default {QuickAdapterV3.default_reversal_confirmation['lookback_period_candles']!r}"
)
- lookback_period = QuickAdapterV3.default_reversal_confirmation[
- "lookback_period"
+ lookback_period_candles = QuickAdapterV3.default_reversal_confirmation[
+ "lookback_period_candles"
]
- if not isinstance(decay_ratio, (int, float)) or not (0.0 < decay_ratio <= 1.0):
+ if not isinstance(decay_fraction, (int, float)) or not (
+ 0.0 < decay_fraction <= 1.0
+ ):
logger.warning(
- f"Invalid reversal_confirmation decay_ratio {decay_ratio!r}: must be in range (0, 1]. Using default {QuickAdapterV3.default_reversal_confirmation['decay_ratio']!r}"
+ f"Invalid reversal_confirmation decay_fraction {decay_fraction!r}: must be in range (0, 1]. Using default {QuickAdapterV3.default_reversal_confirmation['decay_fraction']!r}"
)
- decay_ratio = QuickAdapterV3.default_reversal_confirmation["decay_ratio"]
+ decay_fraction = QuickAdapterV3.default_reversal_confirmation[
+ "decay_fraction"
+ ]
- min_natr_ratio_percent, max_natr_ratio_percent = validate_range(
- min_natr_ratio_percent,
- max_natr_ratio_percent,
+ min_natr_multiplier_fraction, max_natr_multiplier_fraction = validate_range(
+ min_natr_multiplier_fraction,
+ max_natr_multiplier_fraction,
logger,
- name="natr_ratio_percent",
+ name="natr_multiplier_fraction",
default_min=QuickAdapterV3.default_reversal_confirmation[
- "min_natr_ratio_percent"
+ "min_natr_multiplier_fraction"
],
default_max=QuickAdapterV3.default_reversal_confirmation[
- "max_natr_ratio_percent"
+ "max_natr_multiplier_fraction"
],
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)
+ self._reversal_lookback_period_candles = int(lookback_period_candles)
+ self._reversal_decay_fraction = float(decay_fraction)
+ self._reversal_min_natr_multiplier_fraction = float(
+ min_natr_multiplier_fraction
+ )
+ self._reversal_max_natr_multiplier_fraction = float(
+ max_natr_multiplier_fraction
+ )
def feature_engineering_expand_all(
self, dataframe: DataFrame, period: int, metadata: dict[str, Any], **kwargs
if isinstance(label_period_candles, int):
self._label_params[pair]["label_period_candles"] = label_period_candles
- def get_label_natr_ratio(self, pair: str) -> float:
- label_natr_ratio = self._label_params.get(pair, {}).get("label_natr_ratio")
- if label_natr_ratio and isinstance(label_natr_ratio, float):
- return label_natr_ratio
+ def get_label_natr_multiplier(self, pair: str) -> float:
+ label_natr_multiplier = self._label_params.get(pair, {}).get(
+ "label_natr_multiplier"
+ )
+ if label_natr_multiplier and isinstance(label_natr_multiplier, float):
+ return label_natr_multiplier
+ feature_parameters = self.freqai_info.get("feature_parameters", {})
return float(
- self.freqai_info.get("feature_parameters", {}).get(
- "label_natr_ratio",
- self._default_label_natr_ratio,
+ feature_parameters.get(
+ "label_natr_multiplier", self._default_label_natr_multiplier
)
)
- def set_label_natr_ratio(self, pair: str, label_natr_ratio: float) -> None:
- if isinstance(label_natr_ratio, float) and np.isfinite(label_natr_ratio):
- self._label_params[pair]["label_natr_ratio"] = label_natr_ratio
+ def set_label_natr_multiplier(
+ self, pair: str, label_natr_multiplier: float
+ ) -> None:
+ if isinstance(label_natr_multiplier, float) and np.isfinite(
+ label_natr_multiplier
+ ):
+ self._label_params[pair]["label_natr_multiplier"] = label_natr_multiplier
- def get_label_natr_ratio_percent(self, pair: str, percent: float) -> float:
- if not isinstance(percent, float) or not (0.0 <= percent <= 1.0):
+ def get_label_natr_multiplier_fraction(self, pair: str, fraction: float) -> float:
+ if not isinstance(fraction, float) or not (0.0 <= fraction <= 1.0):
raise ValueError(
- f"Invalid percent {percent!r}: must be a float in range [0, 1]"
+ f"Invalid fraction {fraction!r}: must be a float in range [0, 1]"
)
- return self.get_label_natr_ratio(pair) * percent
+ return self.get_label_natr_multiplier(pair) * fraction
@staticmethod
def _get_extrema_weighting_params(
)
smoothing_method = SMOOTHING_METHODS[0]
- smoothing_window = extrema_smoothing.get(
- "window", DEFAULTS_EXTREMA_SMOOTHING["window"]
+ smoothing_window_candles = get_config_value(
+ extrema_smoothing,
+ new_key="window_candles",
+ old_key="window",
+ default=DEFAULTS_EXTREMA_SMOOTHING["window_candles"],
+ logger=logger,
+ new_path="freqai.extrema_smoothing.window_candles",
+ old_path="freqai.extrema_smoothing.window",
)
- if not isinstance(smoothing_window, int) or smoothing_window < 3:
+ if (
+ not isinstance(smoothing_window_candles, int)
+ or smoothing_window_candles < 3
+ ):
logger.warning(
- f"Invalid extrema_smoothing window {smoothing_window!r}: must be an integer >= 3. Using default {DEFAULTS_EXTREMA_SMOOTHING['window']!r}"
+ f"Invalid extrema_smoothing window_candles {smoothing_window_candles!r}: must be an integer >= 3. Using default {DEFAULTS_EXTREMA_SMOOTHING['window_candles']!r}"
)
- smoothing_window = DEFAULTS_EXTREMA_SMOOTHING["window"]
+ smoothing_window_candles = int(DEFAULTS_EXTREMA_SMOOTHING["window_candles"])
smoothing_beta = extrema_smoothing.get(
"beta", DEFAULTS_EXTREMA_SMOOTHING["beta"]
return {
"method": smoothing_method,
- "window": int(smoothing_window),
+ "window_candles": int(smoothing_window_candles),
"beta": smoothing_beta,
"polyorder": int(smoothing_polyorder),
"mode": smoothing_mode,
) -> DataFrame:
pair = str(metadata.get("pair"))
label_period_candles = self.get_label_period_candles(pair)
- label_natr_ratio = self.get_label_natr_ratio(pair)
+ label_natr_multiplier = self.get_label_natr_multiplier(pair)
(
pivots_indices,
_,
) = zigzag(
dataframe,
natr_period=label_period_candles,
- natr_ratio=label_natr_ratio,
+ natr_multiplier=label_natr_multiplier,
)
label_period = datetime.timedelta(
minutes=len(dataframe) * timeframe_to_minutes(self.config.get("timeframe"))
if len(pivots_indices) == 0:
logger.warning(
- f"[{pair}] No extrema to label | label_period: {QuickAdapterV3._td_format(label_period)} | label_period_candles: {label_period_candles} | label_natr_ratio: {format_number(label_natr_ratio)}"
+ f"[{pair}] No extrema to label | label_period: {QuickAdapterV3._td_format(label_period)} | label_period_candles: {label_period_candles} | label_natr_multiplier: {format_number(label_natr_multiplier)}"
)
else:
logger.info(
- f"[{pair}] Labeled {len(pivots_indices)} extrema | label_period: {QuickAdapterV3._td_format(label_period)} | label_period_candles: {label_period_candles} | label_natr_ratio: {format_number(label_natr_ratio)}"
+ f"[{pair}] Labeled {len(pivots_indices)} extrema | label_period: {QuickAdapterV3._td_format(label_period)} | label_period_candles: {label_period_candles} | label_natr_multiplier: {format_number(label_natr_multiplier)}"
)
dataframe.loc[pivots_indices, EXTREMA_COLUMN] = pivots_directions
dataframe[EXTREMA_COLUMN] = smooth_extrema(
weighted_extrema,
self.extrema_smoothing["method"],
- self.extrema_smoothing["window"],
+ self.extrema_smoothing["window_candles"],
self.extrema_smoothing["beta"],
self.extrema_smoothing["polyorder"],
self.extrema_smoothing["mode"],
) -> DataFrame:
dataframe = self.freqai.start(dataframe, metadata, self)
- dataframe["DI_catch"] = np.where(
- dataframe.get("DI_values") > dataframe.get("DI_cutoff"),
- 0,
- 1,
- )
+ di_values = dataframe.get("DI_values")
+ di_cutoff = dataframe.get("DI_cutoff")
+ if di_values is not None and di_cutoff is not None:
+ dataframe["DI_catch"] = np.where(di_values > di_cutoff, 0, 1)
+ else:
+ dataframe["DI_catch"] = 1
pair = str(metadata.get("pair"))
- self.set_label_period_candles(
- pair, dataframe.get("label_period_candles").iloc[-1]
- )
- self.set_label_natr_ratio(pair, dataframe.get("label_natr_ratio").iloc[-1])
+ label_period_candles_series = dataframe.get("label_period_candles")
+ if label_period_candles_series is not None:
+ self.set_label_period_candles(pair, label_period_candles_series.iloc[-1])
+ label_natr_multiplier_series = dataframe.get("label_natr_multiplier")
+ if label_natr_multiplier_series is not None:
+ self.set_label_natr_multiplier(pair, label_natr_multiplier_series.iloc[-1])
dataframe["natr_label_period_candles"] = ta.NATR(
dataframe, timeperiod=self.get_label_period_candles(pair)
def get_trade_natr(
self, df: DataFrame, trade: Trade, trade_duration_candles: int
) -> Optional[float]:
- trade_price_target = self.config.get("exit_pricing", {}).get(
- "trade_price_target",
- TRADE_PRICE_TARGETS[0], # "moving_average"
+ exit_pricing = self.config.get("exit_pricing", {})
+ trade_price_target_method = get_config_value(
+ exit_pricing,
+ new_key="trade_price_target_method",
+ old_key="trade_price_target",
+ default=TRADE_PRICE_TARGETS[0], # "moving_average"
+ logger=logger,
+ new_path="exit_pricing.trade_price_target_method",
+ old_path="exit_pricing.trade_price_target",
)
trade_price_target_methods: dict[str, Callable[[], Optional[float]]] = {
# 0 - "moving_average"
df, trade
),
}
- trade_price_target_fn = trade_price_target_methods.get(trade_price_target)
- if trade_price_target_fn is None:
+ trade_price_target_method_fn = trade_price_target_methods.get(
+ trade_price_target_method
+ )
+ if trade_price_target_method_fn is None:
raise ValueError(
- f"Invalid trade_price_target {trade_price_target!r}. "
+ f"Invalid trade_price_target_method {trade_price_target_method!r}. "
f"Supported: {', '.join(TRADE_PRICE_TARGETS)}"
)
- return trade_price_target_fn()
+ return trade_price_target_method_fn()
@staticmethod
def get_trade_exit_stage(trade: Trade) -> int:
df: DataFrame,
trade: Trade,
current_rate: float,
- natr_ratio_percent: float,
+ natr_multiplier_fraction: float,
) -> Optional[float]:
- if not (0.0 <= natr_ratio_percent <= 1.0):
+ if not (0.0 <= natr_multiplier_fraction <= 1.0):
raise ValueError(
- f"Invalid natr_ratio_percent {natr_ratio_percent!r}: must be in range [0, 1]"
+ f"Invalid natr_multiplier_fraction {natr_multiplier_fraction!r}: must be in range [0, 1]"
)
trade_duration_candles = self.get_trade_duration_candles(df, trade)
if not QuickAdapterV3.is_trade_duration_valid(trade_duration_candles):
return (
current_rate
* (trade_natr / 100.0)
- * self.get_label_natr_ratio_percent(trade.pair, natr_ratio_percent)
+ * self.get_label_natr_multiplier_fraction(
+ trade.pair, natr_multiplier_fraction
+ )
* QuickAdapterV3.get_stoploss_factor(
trade_duration_candles + int(round(trade.nr_of_successful_exits**1.5))
)
return math.log10(9.75 + 0.25 * trade_duration_candles)
def get_take_profit_distance(
- self, df: DataFrame, trade: Trade, natr_ratio_percent: float
+ self, df: DataFrame, trade: Trade, natr_multiplier_fraction: float
) -> Optional[float]:
- if not (0.0 <= natr_ratio_percent <= 1.0):
+ if not (0.0 <= natr_multiplier_fraction <= 1.0):
raise ValueError(
- f"Invalid natr_ratio_percent {natr_ratio_percent!r}: must be in range [0, 1]"
+ f"Invalid natr_multiplier_fraction {natr_multiplier_fraction!r}: must be in range [0, 1]"
)
trade_duration_candles = self.get_trade_duration_candles(df, trade)
if not QuickAdapterV3.is_trade_duration_valid(trade_duration_candles):
return (
trade.open_rate
* (trade_natr / 100.0)
- * self.get_label_natr_ratio_percent(trade.pair, natr_ratio_percent)
+ * self.get_label_natr_multiplier_fraction(
+ trade.pair, natr_multiplier_fraction
+ )
* QuickAdapterV3.get_take_profit_factor(trade_duration_candles)
)
return None
stoploss_distance = self.get_stoploss_distance(
- df, trade, current_rate, QuickAdapterV3._CUSTOM_STOPLOSS_NATR_RATIO_PERCENT
+ df,
+ trade,
+ current_rate,
+ QuickAdapterV3._CUSTOM_STOPLOSS_NATR_MULTIPLIER_FRACTION,
)
if isna(stoploss_distance) or stoploss_distance <= 0:
return None
def get_take_profit_price(
self, df: DataFrame, trade: Trade, exit_stage: int
) -> Optional[float]:
- natr_ratio_percent = (
+ natr_multiplier_fraction = (
QuickAdapterV3.partial_exit_stages[exit_stage][0]
if exit_stage in QuickAdapterV3.partial_exit_stages
else QuickAdapterV3._FINAL_EXIT_STAGE[0]
)
take_profit_distance = self.get_take_profit_distance(
- df, trade, natr_ratio_percent
+ df, trade, natr_multiplier_fraction
)
if isna(take_profit_distance) or take_profit_distance <= 0:
return None
self,
df: DataFrame,
pair: str,
- min_natr_ratio_percent: float,
- max_natr_ratio_percent: float,
+ min_natr_multiplier_fraction: float,
+ max_natr_multiplier_fraction: float,
candle_idx: int = -1,
interpolation_direction: InterpolationDirection = "direct",
quantile_exponent: float = 1.5,
cache_key: CandleDeviationCacheKey = (
pair,
df_signature,
- float(min_natr_ratio_percent),
- float(max_natr_ratio_percent),
+ float(min_natr_multiplier_fraction),
+ float(max_natr_multiplier_fraction),
candle_idx,
interpolation_direction,
float(quantile_exponent),
if (
interpolation_direction == QuickAdapterV3._INTERPOLATION_DIRECTIONS[0]
): # "direct"
- natr_ratio_percent = (
- min_natr_ratio_percent
- + (max_natr_ratio_percent - min_natr_ratio_percent)
+ natr_multiplier_fraction = (
+ min_natr_multiplier_fraction
+ + (max_natr_multiplier_fraction - min_natr_multiplier_fraction)
* candle_label_natr_value_quantile**quantile_exponent
)
elif (
interpolation_direction == QuickAdapterV3._INTERPOLATION_DIRECTIONS[1]
): # "inverse"
- natr_ratio_percent = (
- max_natr_ratio_percent
- - (max_natr_ratio_percent - min_natr_ratio_percent)
+ natr_multiplier_fraction = (
+ max_natr_multiplier_fraction
+ - (max_natr_multiplier_fraction - min_natr_multiplier_fraction)
* candle_label_natr_value_quantile**quantile_exponent
)
else:
)
candle_deviation = (
candle_label_natr_value / 100.0
- ) * self.get_label_natr_ratio_percent(pair, natr_ratio_percent)
+ ) * self.get_label_natr_multiplier_fraction(pair, natr_multiplier_fraction)
self._candle_deviation_cache[cache_key] = candle_deviation
return self._candle_deviation_cache[cache_key]
df: DataFrame,
pair: str,
side: TradeDirection,
- min_natr_ratio_percent: float,
- max_natr_ratio_percent: float,
+ min_natr_multiplier_fraction: float,
+ max_natr_multiplier_fraction: float,
candle_idx: int = -1,
) -> float:
df_signature = QuickAdapterV3._df_signature(df)
df_signature,
side,
candle_idx,
- float(min_natr_ratio_percent),
- float(max_natr_ratio_percent),
+ float(min_natr_multiplier_fraction),
+ float(max_natr_multiplier_fraction),
)
if cache_key in self._candle_threshold_cache:
return self._candle_threshold_cache[cache_key]
current_deviation = self._calculate_candle_deviation(
df,
pair,
- min_natr_ratio_percent=min_natr_ratio_percent,
- max_natr_ratio_percent=max_natr_ratio_percent,
+ min_natr_multiplier_fraction=min_natr_multiplier_fraction,
+ max_natr_multiplier_fraction=max_natr_multiplier_fraction,
candle_idx=candle_idx,
interpolation_direction=QuickAdapterV3._INTERPOLATION_DIRECTIONS[
0
side: TradeDirection,
order: OrderType,
rate: float,
- lookback_period: int,
- decay_ratio: float,
- min_natr_ratio_percent: float,
- max_natr_ratio_percent: float,
+ lookback_period_candles: int,
+ decay_fraction: float,
+ min_natr_multiplier_fraction: float,
+ max_natr_multiplier_fraction: float,
) -> bool:
"""Confirm a directional reversal using a volatility-adaptive current-candle
threshold and optionally a backward confirmation chain with geometric decay.
--------
1. Compute a deviation-based threshold on the latest candle (-1). The current
rate must strictly break it (long: rate > threshold; short: rate < threshold).
- 2. If lookback_period > 0, for each k = 1..lookback_period:
- - Decay (min_natr_ratio_percent, max_natr_ratio_percent) by (decay_ratio ** k),
- clamped to [0, 1].
+ 2. If lookback_period_candles > 0, for each k = 1..lookback_period_candles:
+ - Decay (min_natr_multiplier_fraction, max_natr_multiplier_fraction) by
+ (decay_fraction ** k), clamped to [0, 1].
- Recompute the threshold on candle index -(k+1).
- Require close[-k] to have strictly broken that historical threshold.
3. If an intermediate close or threshold is non-finite, chain evaluation aborts
Context (affects log wording only).
rate : float
Candidate execution price; must break the current threshold.
- lookback_period : int
+ lookback_period_candles : int
Number of historical confirmation steps requested; truncated to history.
- decay_ratio : float
- Geometric decay factor per step (0 < decay_ratio <= 1); 1.0 disables decay.
- min_natr_ratio_percent : float
- Lower bound fraction (e.g. 0.009 = 0.9%).
- max_natr_ratio_percent : float
- Upper bound fraction (>= lower bound).
+ decay_fraction : float
+ Geometric decay factor per step (0 < decay_fraction <= 1); 1.0 disables decay.
+ min_natr_multiplier_fraction : float
+ Lower-bound fraction (e.g. 0.009 = 0.9%).
+ max_natr_multiplier_fraction : float
+ Upper-bound fraction (>= lower bound).
Returns
-------
Fallback Semantics
------------------
- Missing / non-finite intermediate data ⇒ stop chain; return current candle result.
+ Missing / non-finite intermediate data -> stop chain; return current candle result.
This may yield True on partial history, weakening strict multi-candle guarantees.
Rejection Conditions
--------------------
Empty dataframe, invalid side/order, non-finite rate, negative lookback,
- decay_ratio outside (0,1], invalid min/max ordering, failure to break current
+ decay_fraction outside (0,1], invalid min/max ordering, failure to break current
threshold, or failed historical step comparison.
Complexity
----------
- O(lookback_period) threshold computations.
+ O(lookback_period_candles) threshold computations.
Logging
-------
- Logs rejection reasons (invalid decay_ratio, threshold not broken, failed step).
+ Logs rejection reasons (invalid decay_fraction, threshold not broken, failed step).
Fallback aborts are silent.
Limitations
if not isinstance(rate, (int, float)) or not np.isfinite(rate):
return False
if (
- not isinstance(min_natr_ratio_percent, (int, float))
- or not isinstance(max_natr_ratio_percent, (int, float))
- or not np.isfinite(min_natr_ratio_percent)
- or not np.isfinite(max_natr_ratio_percent)
- or min_natr_ratio_percent < 0
- or max_natr_ratio_percent < 0
- or min_natr_ratio_percent > max_natr_ratio_percent
+ not isinstance(min_natr_multiplier_fraction, (int, float))
+ or not isinstance(max_natr_multiplier_fraction, (int, float))
+ or not np.isfinite(min_natr_multiplier_fraction)
+ or not np.isfinite(max_natr_multiplier_fraction)
+ or min_natr_multiplier_fraction < 0
+ or max_natr_multiplier_fraction < 0
+ or min_natr_multiplier_fraction > max_natr_multiplier_fraction
):
return False
trade_direction = side
max_lookback_period = max(0, len(df) - 1)
- if lookback_period > max_lookback_period:
- lookback_period = max_lookback_period
- if not isinstance(decay_ratio, (int, float)):
+ lookback_period_candles = min(lookback_period_candles, max_lookback_period)
+ if not isinstance(decay_fraction, (int, float)):
logger.debug(
- f"[{pair}] Denied {trade_direction} {order}: invalid decay_ratio type"
+ f"[{pair}] Denied {trade_direction} {order}: invalid decay_fraction type"
)
return False
- if not (0.0 < decay_ratio <= 1.0):
+ if not (0.0 < decay_fraction <= 1.0):
logger.debug(
- f"[{pair}] Denied {trade_direction} {order}: invalid decay_ratio {decay_ratio}, must be in (0, 1]"
+ f"[{pair}] Denied {trade_direction} {order}: invalid decay_fraction {decay_fraction}, must be in (0, 1]"
)
return False
df,
pair,
side,
- min_natr_ratio_percent=min_natr_ratio_percent,
- max_natr_ratio_percent=max_natr_ratio_percent,
+ min_natr_multiplier_fraction=min_natr_multiplier_fraction,
+ max_natr_multiplier_fraction=max_natr_multiplier_fraction,
candle_idx=-1,
)
current_ok = np.isfinite(current_threshold) and (
)
return False
- if lookback_period == 0:
+ if lookback_period_candles == 0:
return current_ok
- for k in range(1, lookback_period + 1):
+ for k in range(1, lookback_period_candles + 1):
close_k = df.iloc[-k].get("close")
if not isinstance(close_k, (int, float)) or not np.isfinite(close_k):
return current_ok
- decay_factor = decay_ratio**k
- decayed_min_natr_ratio_percent = max(
- 0.0, min(1.0, min_natr_ratio_percent * decay_factor)
+ decay_factor = decay_fraction**k
+ decayed_min_natr_multiplier_fraction = max(
+ 0.0, min(1.0, min_natr_multiplier_fraction * decay_factor)
)
- decayed_max_natr_ratio_percent = max(
- decayed_min_natr_ratio_percent,
- min(1.0, max_natr_ratio_percent * decay_factor),
+ decayed_max_natr_multiplier_fraction = max(
+ decayed_min_natr_multiplier_fraction,
+ min(1.0, max_natr_multiplier_fraction * decay_factor),
)
threshold_k = self._calculate_candle_threshold(
df,
pair,
side,
- min_natr_ratio_percent=decayed_min_natr_ratio_percent,
- max_natr_ratio_percent=decayed_max_natr_ratio_percent,
+ min_natr_multiplier_fraction=decayed_min_natr_multiplier_fraction,
+ max_natr_multiplier_fraction=decayed_max_natr_multiplier_fraction,
candle_idx=-(k + 1),
)
if not isinstance(threshold_k, (int, float)) or not np.isfinite(
f"[{pair}] Denied {trade_direction} {order}: "
f"close_k[{-k}] {format_number(close_k)} "
f"did not break threshold_k[{-(k + 1)}] {format_number(threshold_k)} "
- f"(decayed natr_ratio_percent: min={format_number(decayed_min_natr_ratio_percent)}, max={format_number(decayed_max_natr_ratio_percent)})"
+ f"(decayed natr_multiplier_fraction: min={format_number(decayed_min_natr_multiplier_fraction)}, max={format_number(decayed_max_natr_multiplier_fraction)})"
)
return False
QuickAdapterV3._TRADE_DIRECTIONS[0], # "long"
QuickAdapterV3._ORDER_TYPES[1], # "exit"
current_rate,
- self._reversal_lookback_period,
- self._reversal_decay_ratio,
- self._reversal_min_natr_ratio_percent,
- self._reversal_max_natr_ratio_percent,
+ self._reversal_lookback_period_candles,
+ self._reversal_decay_fraction,
+ self._reversal_min_natr_multiplier_fraction,
+ self._reversal_max_natr_multiplier_fraction,
)
):
return "minima_detected_short"
QuickAdapterV3._TRADE_DIRECTIONS[1], # "short"
QuickAdapterV3._ORDER_TYPES[1], # "exit"
current_rate,
- self._reversal_lookback_period,
- self._reversal_decay_ratio,
- self._reversal_min_natr_ratio_percent,
- self._reversal_max_natr_ratio_percent,
+ self._reversal_lookback_period_candles,
+ self._reversal_decay_fraction,
+ self._reversal_min_natr_multiplier_fraction,
+ self._reversal_max_natr_multiplier_fraction,
)
):
return "maxima_detected_long"
side,
QuickAdapterV3._ORDER_TYPES[0], # "entry"
rate,
- self._reversal_lookback_period,
- self._reversal_decay_ratio,
- self._reversal_min_natr_ratio_percent,
- self._reversal_max_natr_ratio_percent,
+ self._reversal_lookback_period_candles,
+ self._reversal_decay_fraction,
+ self._reversal_min_natr_multiplier_fraction,
+ self._reversal_max_natr_multiplier_fraction,
):
return True
return False
DEFAULTS_EXTREMA_SMOOTHING: Final[dict[str, Any]] = {
"method": SMOOTHING_METHODS[0], # "gaussian"
- "window": 5,
+ "window_candles": 5,
"beta": 8.0,
"polyorder": 3,
"mode": SMOOTHING_MODES[0], # "mirror"
def midpoint(value1: T, value2: T) -> T:
- """Calculate the midpoint (geometric center) between two values."""
+ """Calculate the midpoint between two values."""
return (value1 + value2) / 2
def smooth_extrema(
series: pd.Series,
method: SmoothingMethod = DEFAULTS_EXTREMA_SMOOTHING["method"],
- window: int = DEFAULTS_EXTREMA_SMOOTHING["window"],
+ window: int = DEFAULTS_EXTREMA_SMOOTHING["window_candles"],
beta: float = DEFAULTS_EXTREMA_SMOOTHING["beta"],
polyorder: int = DEFAULTS_EXTREMA_SMOOTHING["polyorder"],
mode: SmoothingMode = DEFAULTS_EXTREMA_SMOOTHING["mode"],
def zigzag(
df: pd.DataFrame,
natr_period: int = 14,
- natr_ratio: float = 9.0,
+ natr_multiplier: float = 9.0,
) -> tuple[
list[int],
list[float],
natr_values = (ta.NATR(df, timeperiod=natr_period).bfill() / 100.0).to_numpy()
indices: list[int] = df.index.tolist()
- thresholds: NDArray[np.floating] = natr_values * natr_ratio
+ thresholds: NDArray[np.floating] = natr_values * natr_multiplier
closes = df.get("close").to_numpy()
log_closes = np.log(closes)
highs = df.get("high").to_numpy()
return model
+def _build_int_range(
+ frange: tuple[float, float],
+ min_val: int = 1,
+) -> tuple[int, int]:
+ lo, hi = math.ceil(frange[0]), math.floor(frange[1])
+ if lo > hi:
+ lo = hi = max(min_val, int(round((frange[0] + frange[1]) / 2)))
+ return max(min_val, lo), max(min_val, hi)
+
+
+def _optuna_suggest_int_from_range(
+ trial: optuna.trial.Trial,
+ name: str,
+ frange: tuple[float, float],
+ *,
+ min_val: int = 1,
+ log: bool = False,
+) -> int:
+ int_range = _build_int_range(frange, min_val=min_val)
+ return trial.suggest_int(name, int_range[0], int_range[1], log=log)
+
+
def get_optuna_study_model_parameters(
trial: optuna.trial.Trial,
regressor: Regressor,
model_training_best_parameters: dict[str, Any],
space_reduction: bool,
- expansion_ratio: float,
+ space_fraction: float,
) -> dict[str, Any]:
if regressor not in set(REGRESSORS):
raise ValueError(
f"Invalid regressor {regressor!r}. Supported: {', '.join(REGRESSORS)}"
)
- if not isinstance(expansion_ratio, (int, float)) or not (
- 0.0 <= expansion_ratio <= 1.0
+ if not isinstance(space_fraction, (int, float)) or not (
+ 0.0 <= space_fraction <= 1.0
):
raise ValueError(
- f"Invalid expansion_ratio {expansion_ratio!r}: must be in range [0, 1]"
+ f"Invalid space_fraction {space_fraction!r}: must be in range [0, 1]"
)
def _build_ranges(
if param in log_scaled_params:
if center_value <= 0:
continue
- factor = 1 + expansion_ratio
+ # Proportional reduction in log-space
+ factor = math.pow(default_max / default_min, space_fraction / 2)
new_min = center_value / factor
new_max = center_value * factor
else:
- margin = (default_max - default_min) * expansion_ratio / 2
+ margin = (default_max - default_min) * space_fraction / 2
new_min = center_value - margin
new_max = center_value + margin
param_min = max(default_min, new_min)
ranges = _build_ranges(default_ranges, log_scaled_params)
- tree_method = trial.suggest_categorical("tree_method", ["hist", "approx"])
grow_policy = trial.suggest_categorical(
"grow_policy", ["depthwise", "lossguide"]
)
+ tree_method = (
+ "hist"
+ if grow_policy == "lossguide"
+ else trial.suggest_categorical("tree_method", ["hist", "approx"])
+ )
return {
# Boosting/Training
- "n_estimators": trial.suggest_int(
- "n_estimators",
- int(ranges["n_estimators"][0]),
- int(ranges["n_estimators"][1]),
- log=True,
+ "n_estimators": _optuna_suggest_int_from_range(
+ trial, "n_estimators", ranges["n_estimators"], min_val=1, log=True
),
"learning_rate": trial.suggest_float(
"learning_rate",
**(
{
"max_depth": 0,
- "max_leaves": trial.suggest_int(
- "max_leaves",
- int(ranges["max_leaves"][0]),
- int(ranges["max_leaves"][1]),
- log=True,
+ "max_leaves": _optuna_suggest_int_from_range(
+ trial, "max_leaves", ranges["max_leaves"], min_val=2, log=True
),
}
if grow_policy == "lossguide"
else {
- "max_depth": trial.suggest_int(
- "max_depth",
- int(ranges["max_depth"][0]),
- int(ranges["max_depth"][1]),
+ "max_depth": _optuna_suggest_int_from_range(
+ trial, "max_depth", ranges["max_depth"], min_val=1
),
}
),
return {
# Boosting/Training
- "n_estimators": trial.suggest_int(
- "n_estimators",
- int(ranges["n_estimators"][0]),
- int(ranges["n_estimators"][1]),
- log=True,
+ "n_estimators": _optuna_suggest_int_from_range(
+ trial, "n_estimators", ranges["n_estimators"], min_val=1, log=True
),
"learning_rate": trial.suggest_float(
"learning_rate",
log=True,
),
# Tree structure
- "num_leaves": trial.suggest_int(
- "num_leaves",
- int(ranges["num_leaves"][0]),
- int(ranges["num_leaves"][1]),
+ "num_leaves": _optuna_suggest_int_from_range(
+ trial, "num_leaves", ranges["num_leaves"], min_val=2
),
# Leaf constraints
"min_child_weight": trial.suggest_float(
ranges["min_child_weight"][1],
log=True,
),
- "min_child_samples": trial.suggest_int(
- "min_child_samples",
- int(ranges["min_child_samples"][0]),
- int(ranges["min_child_samples"][1]),
+ "min_child_samples": _optuna_suggest_int_from_range(
+ trial, "min_child_samples", ranges["min_child_samples"], min_val=1
),
"min_split_gain": trial.suggest_float(
"min_split_gain",
"subsample": trial.suggest_float(
"subsample", ranges["subsample"][0], ranges["subsample"][1]
),
- "subsample_freq": trial.suggest_int(
- "subsample_freq",
- int(ranges["subsample_freq"][0]),
- int(ranges["subsample_freq"][1]),
+ "subsample_freq": _optuna_suggest_int_from_range(
+ trial, "subsample_freq", ranges["subsample_freq"], min_val=1
),
"colsample_bytree": trial.suggest_float(
"colsample_bytree",
"reg_lambda", ranges["reg_lambda"][0], ranges["reg_lambda"][1], log=True
),
# Binning
- "max_bin": trial.suggest_int(
- "max_bin",
- int(ranges["max_bin"][0]),
- int(ranges["max_bin"][1]),
+ "max_bin": _optuna_suggest_int_from_range(
+ trial, "max_bin", ranges["max_bin"], min_val=2
),
}
return {
# Boosting/Training
- "max_iter": trial.suggest_int(
- "max_iter",
- int(ranges["max_iter"][0]),
- int(ranges["max_iter"][1]),
- log=True,
+ "max_iter": _optuna_suggest_int_from_range(
+ trial, "max_iter", ranges["max_iter"], min_val=1, log=True
),
"learning_rate": trial.suggest_float(
"learning_rate",
"max_depth": trial.suggest_categorical(
"max_depth", [None, 2, 3, 4, 5, 6, 7, 8, 10, 12, 15]
),
- "max_leaf_nodes": trial.suggest_int(
- "max_leaf_nodes",
- int(ranges["max_leaf_nodes"][0]),
- int(ranges["max_leaf_nodes"][1]),
- log=True,
+ "max_leaf_nodes": _optuna_suggest_int_from_range(
+ trial, "max_leaf_nodes", ranges["max_leaf_nodes"], min_val=2, log=True
),
# Leaf constraints
- "min_samples_leaf": trial.suggest_int(
+ "min_samples_leaf": _optuna_suggest_int_from_range(
+ trial,
"min_samples_leaf",
- int(ranges["min_samples_leaf"][0]),
- int(ranges["min_samples_leaf"][1]),
+ ranges["min_samples_leaf"],
+ min_val=1,
log=True,
),
# Sampling
# Regularization
"l2_regularization": l2_regularization,
# Binning
- "max_bins": trial.suggest_int(
- "max_bins",
- int(ranges["max_bins"][0]),
- int(ranges["max_bins"][1]),
+ "max_bins": _optuna_suggest_int_from_range(
+ trial, "max_bins", ranges["max_bins"], min_val=2
),
# Early stopping
- "n_iter_no_change": trial.suggest_int(
- "n_iter_no_change",
- int(ranges["n_iter_no_change"][0]),
- int(ranges["n_iter_no_change"][1]),
+ "n_iter_no_change": _optuna_suggest_int_from_range(
+ trial, "n_iter_no_change", ranges["n_iter_no_change"], min_val=1
),
"tol": trial.suggest_float(
"tol",
return int(math.floor(float(value) / step) * step)
+def get_config_value(
+ config: Any,
+ *,
+ new_key: str,
+ old_key: str,
+ default: Any,
+ logger: Logger,
+ new_path: str,
+ old_path: str,
+) -> Any:
+ if not isinstance(config, dict):
+ return default
+
+ if new_key in config:
+ return config[new_key]
+
+ if old_key in config:
+ logger.warning(
+ f"Deprecated config key {old_path} detected; use {new_path} instead"
+ )
+ config[new_key] = config.pop(old_key)
+ return config[new_key]
+
+ config[new_key] = default
+ return default
+
+
def validate_range(
min_val: float | int,
max_val: float | int,
*,
default_min_label_period_candles: int = 12,
default_max_label_period_candles: int = 24,
- default_min_label_natr_ratio: float = 9.0,
- default_max_label_natr_ratio: float = 12.0,
+ default_min_label_natr_multiplier: float = 9.0,
+ default_max_label_natr_multiplier: float = 12.0,
) -> tuple[float, int]:
- min_label_natr_ratio = feature_parameters.get(
- "min_label_natr_ratio", default_min_label_natr_ratio
+ min_label_natr_multiplier = get_config_value(
+ feature_parameters,
+ new_key="min_label_natr_multiplier",
+ old_key="min_label_natr_ratio",
+ default=default_min_label_natr_multiplier,
+ logger=logger,
+ new_path="freqai.feature_parameters.min_label_natr_multiplier",
+ old_path="freqai.feature_parameters.min_label_natr_ratio",
)
- max_label_natr_ratio = feature_parameters.get(
- "max_label_natr_ratio", default_max_label_natr_ratio
+ max_label_natr_multiplier = get_config_value(
+ feature_parameters,
+ new_key="max_label_natr_multiplier",
+ old_key="max_label_natr_ratio",
+ default=default_max_label_natr_multiplier,
+ logger=logger,
+ new_path="freqai.feature_parameters.max_label_natr_multiplier",
+ old_path="freqai.feature_parameters.max_label_natr_ratio",
)
- min_label_natr_ratio, max_label_natr_ratio = validate_range(
- min_label_natr_ratio,
- max_label_natr_ratio,
+ min_label_natr_multiplier, max_label_natr_multiplier = validate_range(
+ min_label_natr_multiplier,
+ max_label_natr_multiplier,
logger,
- name="label_natr_ratio",
- default_min=default_min_label_natr_ratio,
- default_max=default_max_label_natr_ratio,
+ name="label_natr_multiplier",
+ default_min=default_min_label_natr_multiplier,
+ default_max=default_max_label_natr_multiplier,
allow_equal=False,
non_negative=True,
finite_only=True,
)
- default_label_natr_ratio = float(
- midpoint(min_label_natr_ratio, max_label_natr_ratio)
+ default_label_natr_multiplier = float(
+ midpoint(min_label_natr_multiplier, max_label_natr_multiplier)
+ )
+ get_config_value(
+ feature_parameters,
+ new_key="label_natr_multiplier",
+ old_key="label_natr_ratio",
+ default=default_label_natr_multiplier,
+ logger=logger,
+ new_path="freqai.feature_parameters.label_natr_multiplier",
+ old_path="freqai.feature_parameters.label_natr_ratio",
)
min_label_period_candles = feature_parameters.get(
round(midpoint(min_label_period_candles, max_label_period_candles))
)
- return default_label_natr_ratio, default_label_period_candles
+ return default_label_natr_multiplier, default_label_period_candles