From 286cfda4e18ccb1cd40f8bd631f8ee0f117d617e Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sun, 28 Dec 2025 19:51:56 +0100 Subject: [PATCH] refactor(quickadapter)!: normalize tunables namespace for semantic consistency (#26) MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit * refactor(quickadapter): normalize tunables namespace for semantic consistency Rename config keys and internal variables to follow consistent naming conventions: - `_candles` suffix for time periods in candle units - `_fraction` suffix for values in [0,1] range - `_multiplier` suffix for scaling factors - `_method` suffix for algorithm selectors Config key renames (with backward-compatible deprecated aliases): - lookback_period → lookback_period_candles - decay_ratio → decay_fraction - min/max_natr_ratio_percent → min/max_natr_ratio_fraction - window → window_candles - label_natr_ratio → label_natr_multiplier - threshold_outlier → outlier_threshold_fraction - thresholds_smoothing → threshold_smoothing_method - thresholds_alpha → soft_extremum_alpha - extrema_fraction → keep_extrema_fraction - expansion_ratio → space_fraction - trade_price_target → trade_price_target_method Internal variable renames for code consistency: - threshold_outlier → outlier_threshold_fraction - thresholds_alpha → soft_extremum_alpha - extrema_fraction → keep_extrema_fraction (local vars and function params) - _reversal_lookback_period → _reversal_lookback_period_candles - natr_ratio → natr_multiplier (zigzag function param) All deprecated aliases emit warnings and remain functional for backward compatibility. * chore(quickadapter): remove temporary audit file from codebase * refactor(quickadapter): align constant names with normalized tunables Rename class constants to match the normalized config key names: - PREDICTIONS_EXTREMA_THRESHOLD_OUTLIER_DEFAULT → PREDICTIONS_EXTREMA_OUTLIER_THRESHOLD_FRACTION_DEFAULT - PREDICTIONS_EXTREMA_THRESHOLDS_ALPHA_DEFAULT → PREDICTIONS_EXTREMA_SOFT_EXTREMUM_ALPHA_DEFAULT - PREDICTIONS_EXTREMA_EXTREMA_FRACTION_DEFAULT → PREDICTIONS_EXTREMA_KEEP_EXTREMA_FRACTION_DEFAULT * fix(quickadapter): rename outlier_threshold_fraction to outlier_threshold_quantile The value (e.g., 0.999) represents the 99.9th percentile, which is mathematically a quantile, not a fraction. This aligns the naming with the semantic meaning of the parameter. * fix(quickadapter): add missing deprecated alias support Add backward-compatible deprecated alias handling for: - freqai.optuna_hyperopt.expansion_ratio → space_fraction - freqai.feature_parameters.min_label_natr_ratio → min_label_natr_multiplier - freqai.feature_parameters.max_label_natr_ratio → max_label_natr_multiplier Also add missing deprecated alias documentation in README for: - reversal_confirmation.min_natr_ratio_percent → min_natr_ratio_fraction - reversal_confirmation.max_natr_ratio_percent → max_natr_ratio_fraction This ensures all deprecated aliases mentioned in the commit message of the namespace normalization refactor are properly implemented. * style(readme): normalize trailing whitespace * fix(quickadapter): address PR review feedback - Fix error message referencing 'window' instead of 'window_candles' - Clarify soft_extremum_alpha error message (alpha=0 uses mean) - Improve space_fraction documentation in README - Simplify midpoint docstring * refactor(quickadapter): rename safe configuration value retrieval helper Signed-off-by: Jérôme Benoit * refactor(quickadapter): rename natr_ratio_fraction to natr_multiplier_fraction - Align naming with label_natr_multiplier for consistency - Rename get_config_value_with_deprecated_alias to get_config_value * refactor(quickadapter): centralize label_natr_multiplier migration in get_label_defaults - Move label_natr_ratio -> label_natr_multiplier migration to get_label_defaults() - Update get_config_value to migrate in-place (pop old key, store new key) - Remove redundant get_config_value calls in Strategy and Model __init__ - Simplify cached properties to use .get() since migration is done at init - Rename _CUSTOM_STOPLOSS_NATR_RATIO_FRACTION to _CUSTOM_STOPLOSS_NATR_MULTIPLIER_FRACTION * fix(quickadapter): check that df columns exist before using them Signed-off-by: Jérôme Benoit * docs(README.md): update QuickAdapter strategy documentation Signed-off-by: Jérôme Benoit * chore(quickadapter): bump version to 3.8.0 * refactor(quickadapter): remove unnecessary intermediate variable * refactor(quickadapter): add cached properties for label_period_candles bounds * chore(quickadapter): cleanup docstrings and comments Signed-off-by: Jérôme Benoit --------- Signed-off-by: Jérôme Benoit --- .opencode/command/openspec-apply.md | 4 - .opencode/command/openspec-archive.md | 3 - .opencode/command/openspec-proposal.md | 4 - AGENTS.md | 3 +- README.md | 168 +++---- openspec/project.md | 101 ++-- quickadapter/user_data/config-template.json | 9 +- .../freqaimodels/QuickAdapterRegressorV3.py | 286 +++++++----- .../user_data/strategies/QuickAdapterV3.py | 437 +++++++++++------- quickadapter/user_data/strategies/Utils.py | 213 +++++---- 10 files changed, 718 insertions(+), 510 deletions(-) diff --git a/.opencode/command/openspec-apply.md b/.opencode/command/openspec-apply.md index 2686863..2e25974 100644 --- a/.opencode/command/openspec-apply.md +++ b/.opencode/command/openspec-apply.md @@ -4,16 +4,13 @@ description: Implement an approved OpenSpec change and keep tasks in sync. --- - **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//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. @@ -21,6 +18,5 @@ Track these steps as TODOs and complete them one by one. 5. Reference `openspec list` or `openspec show ` when additional context is required. **Reference** - - Use `openspec show --json --deltas-only` if you need additional context from the proposal while implementing. diff --git a/.opencode/command/openspec-archive.md b/.opencode/command/openspec-archive.md index 8d60d35..d172e44 100644 --- a/.opencode/command/openspec-archive.md +++ b/.opencode/command/openspec-archive.md @@ -1,7 +1,6 @@ --- description: Archive a deployed OpenSpec change and update specs. --- - $ARGUMENTS @@ -12,7 +11,6 @@ description: Archive a deployed OpenSpec change and update specs. - 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 `` 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. @@ -24,7 +22,6 @@ description: Archive a deployed OpenSpec change and update specs. 5. Validate with `openspec validate --strict` and inspect with `openspec show ` 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. diff --git a/.opencode/command/openspec-proposal.md b/.opencode/command/openspec-proposal.md index 9282a4f..47fabaa 100644 --- a/.opencode/command/openspec-proposal.md +++ b/.opencode/command/openspec-proposal.md @@ -9,9 +9,7 @@ $ARGUMENTS - **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. @@ -19,7 +17,6 @@ $ARGUMENTS - 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//`. 3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing. @@ -29,7 +26,6 @@ $ARGUMENTS 7. Validate with `openspec validate --strict` and resolve every issue before sharing the proposal. **Reference** - - Use `openspec show --json --deltas-only` or `openspec show --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 `, `ls`, or direct file reads so proposals align with current implementation realities. diff --git a/AGENTS.md b/AGENTS.md index 3115965..1a672f2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,4 +20,5 @@ Keep this managed block so 'openspec update' can refresh the instructions. -Open `@/.github/copilot-instructions.md`, read it and strictly follow the instructions. +Open `@/.github/copilot-instructions.md`, read it and strictly follow the +instructions. diff --git a/README.md b/README.md index 930eb75..dbae7aa 100644 --- a/README.md +++ b/README.md @@ -37,90 +37,90 @@ docker compose up -d --build ### 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 diff --git a/openspec/project.md b/openspec/project.md index a9e27d7..9b5b4b4 100644 --- a/openspec/project.md +++ b/openspec/project.md @@ -2,13 +2,17 @@ ## 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 @@ -26,54 +30,89 @@ Research, prototype, and refine advanced ML‑driven and RL‑driven trading str ### 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: 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/`, `exp/`. Fix branches should be: `fix/`. -- 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/`, `exp/`. Fix branches should + be: `fix/`. +- 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. diff --git a/quickadapter/user_data/config-template.json b/quickadapter/user_data/config-template.json index de39eda..e5b93d7 100644 --- a/quickadapter/user_data/config-template.json +++ b/quickadapter/user_data/config-template.json @@ -39,7 +39,7 @@ "price_last_balance": 0.0 }, "reversal_confirmation": { - "max_natr_ratio_percent": 0.05 + "max_natr_multiplier_fraction": 0.05 }, "exchange": { "name": "binance", @@ -135,11 +135,11 @@ }, "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, @@ -158,7 +158,7 @@ "&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 }, @@ -174,6 +174,7 @@ "4h" ], "label_period_candles": 18, + "label_natr_multiplier": 10.5, "label_metric": "kmedoids", "include_shifted_candles": 6, "DI_threshold": 10, diff --git a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py index 9a2e364..e81c955 100644 --- a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py +++ b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py @@ -32,6 +32,7 @@ from Utils import ( calculate_n_extrema, fit_regressor, format_number, + get_config_value, get_label_defaults, get_min_max_label_period_candles, get_optuna_callbacks, @@ -71,7 +72,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): https://github.com/sponsors/robcaulk """ - version = "3.7.141" + version = "3.8.0" _TEST_SIZE: Final[float] = 0.1 @@ -170,17 +171,17 @@ class QuickAdapterRegressorV3(BaseRegressionModel): "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 @@ -259,15 +260,53 @@ class QuickAdapterRegressorV3(BaseRegressionModel): "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: """ @@ -322,18 +361,21 @@ class QuickAdapterRegressorV3(BaseRegressionModel): 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( @@ -347,47 +389,63 @@ class QuickAdapterRegressorV3(BaseRegressionModel): ): 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 @@ -438,7 +496,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): 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: @@ -477,10 +535,10 @@ class QuickAdapterRegressorV3(BaseRegressionModel): "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, ) ), } @@ -522,7 +580,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): ) 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')}") @@ -736,16 +794,16 @@ class QuickAdapterRegressorV3(BaseRegressionModel): 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:") @@ -753,17 +811,13 @@ class QuickAdapterRegressorV3(BaseRegressionModel): 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: @@ -773,7 +827,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): 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:") @@ -781,7 +835,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): 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) @@ -950,7 +1004,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): ), # "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, ) @@ -1107,22 +1161,10 @@ class QuickAdapterRegressorV3(BaseRegressionModel): ), 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), ), @@ -1182,7 +1224,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): 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() @@ -1197,10 +1239,12 @@ class QuickAdapterRegressorV3(BaseRegressionModel): 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]) @@ -1258,30 +1302,35 @@ class QuickAdapterRegressorV3(BaseRegressionModel): 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 @@ -1297,15 +1346,15 @@ class QuickAdapterRegressorV3(BaseRegressionModel): 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 ) @@ -1330,15 +1379,15 @@ class QuickAdapterRegressorV3(BaseRegressionModel): 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) ) @@ -1349,7 +1398,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): 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") @@ -1369,7 +1418,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): pred_extrema, minima_indices.size, maxima_indices.size, - extrema_fraction, + keep_extrema_fraction, ) elif ( @@ -1379,7 +1428,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): 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 ( @@ -1430,12 +1479,12 @@ class QuickAdapterRegressorV3(BaseRegressionModel): 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): @@ -1449,10 +1498,10 @@ class QuickAdapterRegressorV3(BaseRegressionModel): 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: @@ -1476,10 +1525,10 @@ class QuickAdapterRegressorV3(BaseRegressionModel): 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: @@ -2622,14 +2671,14 @@ def hp_objective( 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} @@ -2658,8 +2707,8 @@ def label_objective( 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( @@ -2678,8 +2727,11 @@ def label_objective( 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[ @@ -2705,7 +2757,7 @@ def label_objective( ) = 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)) diff --git a/quickadapter/user_data/strategies/QuickAdapterV3.py b/quickadapter/user_data/strategies/QuickAdapterV3.py index e0422cc..2359bf4 100644 --- a/quickadapter/user_data/strategies/QuickAdapterV3.py +++ b/quickadapter/user_data/strategies/QuickAdapterV3.py @@ -48,6 +48,7 @@ from Utils import ( ewo, format_number, get_callable_sha256, + get_config_value, get_distance, get_label_defaults, get_weighted_extrema, @@ -105,7 +106,7 @@ class QuickAdapterV3(IStrategy): _TRADING_MODES: Final[tuple[TradingMode, ...]] = ("spot", "margin", "futures") def version(self) -> str: - return "3.3.191" + return "3.8.0" timeframe = "5m" @@ -122,25 +123,25 @@ class QuickAdapterV3(IStrategy): } 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 @@ -328,7 +329,7 @@ class QuickAdapterV3(IStrategy): / 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]] = {} @@ -341,10 +342,10 @@ class QuickAdapterV3(IStrategy): "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, ) ), } @@ -407,46 +408,56 @@ class QuickAdapterV3(IStrategy): 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:") @@ -477,56 +488,92 @@ class QuickAdapterV3(IStrategy): 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 @@ -710,27 +757,33 @@ class QuickAdapterV3(IStrategy): 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( @@ -996,14 +1049,23 @@ class QuickAdapterV3(IStrategy): ) 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"] @@ -1051,7 +1113,7 @@ class QuickAdapterV3(IStrategy): return { "method": smoothing_method, - "window": int(smoothing_window), + "window_candles": int(smoothing_window_candles), "beta": smoothing_beta, "polyorder": int(smoothing_polyorder), "mode": smoothing_mode, @@ -1080,7 +1142,7 @@ class QuickAdapterV3(IStrategy): ) -> 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, _, @@ -1094,7 +1156,7 @@ class QuickAdapterV3(IStrategy): ) = 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")) @@ -1105,11 +1167,11 @@ class QuickAdapterV3(IStrategy): 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 @@ -1159,7 +1221,7 @@ class QuickAdapterV3(IStrategy): 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"], @@ -1173,18 +1235,21 @@ class QuickAdapterV3(IStrategy): ) -> 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) @@ -1391,9 +1456,15 @@ class QuickAdapterV3(IStrategy): 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" @@ -1409,13 +1480,15 @@ class QuickAdapterV3(IStrategy): 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: @@ -1438,11 +1511,11 @@ class QuickAdapterV3(IStrategy): 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): @@ -1453,7 +1526,9 @@ class QuickAdapterV3(IStrategy): 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)) ) @@ -1465,11 +1540,11 @@ class QuickAdapterV3(IStrategy): 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): @@ -1480,7 +1555,9 @@ class QuickAdapterV3(IStrategy): 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) ) @@ -1533,7 +1610,10 @@ class QuickAdapterV3(IStrategy): 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 @@ -1555,13 +1635,13 @@ class QuickAdapterV3(IStrategy): 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 @@ -1754,8 +1834,8 @@ class QuickAdapterV3(IStrategy): 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, @@ -1770,8 +1850,8 @@ class QuickAdapterV3(IStrategy): 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), @@ -1802,17 +1882,17 @@ class QuickAdapterV3(IStrategy): 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: @@ -1822,7 +1902,7 @@ class QuickAdapterV3(IStrategy): ) 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] @@ -1831,8 +1911,8 @@ class QuickAdapterV3(IStrategy): 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) @@ -1847,16 +1927,16 @@ class QuickAdapterV3(IStrategy): 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 @@ -1903,10 +1983,10 @@ class QuickAdapterV3(IStrategy): 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. @@ -1915,9 +1995,9 @@ class QuickAdapterV3(IStrategy): -------- 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 @@ -1935,14 +2015,14 @@ class QuickAdapterV3(IStrategy): 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 ------- @@ -1952,22 +2032,22 @@ class QuickAdapterV3(IStrategy): 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 @@ -1983,29 +2063,28 @@ class QuickAdapterV3(IStrategy): 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 @@ -2013,8 +2092,8 @@ class QuickAdapterV3(IStrategy): 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 ( @@ -2036,29 +2115,29 @@ class QuickAdapterV3(IStrategy): ) 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( @@ -2077,7 +2156,7 @@ class QuickAdapterV3(IStrategy): 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 @@ -2274,10 +2353,10 @@ class QuickAdapterV3(IStrategy): 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" @@ -2292,10 +2371,10 @@ class QuickAdapterV3(IStrategy): 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" @@ -2455,10 +2534,10 @@ class QuickAdapterV3(IStrategy): 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 diff --git a/quickadapter/user_data/strategies/Utils.py b/quickadapter/user_data/strategies/Utils.py index 4364ee3..98881ad 100644 --- a/quickadapter/user_data/strategies/Utils.py +++ b/quickadapter/user_data/strategies/Utils.py @@ -131,7 +131,7 @@ TRADE_PRICE_TARGETS: Final[tuple[TradePriceTarget, ...]] = ( 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" @@ -167,7 +167,7 @@ def get_distance(p1: T, p2: T) -> T: 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 @@ -265,7 +265,7 @@ def zero_phase_filter( 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"], @@ -1462,7 +1462,7 @@ class TrendDirection(IntEnum): def zigzag( df: pd.DataFrame, natr_period: int = 14, - natr_ratio: float = 9.0, + natr_multiplier: float = 9.0, ) -> tuple[ list[int], list[float], @@ -1491,7 +1491,7 @@ def zigzag( 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() @@ -2085,22 +2085,44 @@ def fit_regressor( 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( @@ -2128,11 +2150,12 @@ def get_optuna_study_model_parameters( 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) @@ -2175,18 +2198,19 @@ def get_optuna_study_model_parameters( 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", @@ -2200,19 +2224,14 @@ def get_optuna_study_model_parameters( **( { "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 ), } ), @@ -2290,11 +2309,8 @@ def get_optuna_study_model_parameters( 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", @@ -2303,10 +2319,8 @@ def get_optuna_study_model_parameters( 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( @@ -2315,10 +2329,8 @@ def get_optuna_study_model_parameters( 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", @@ -2330,10 +2342,8 @@ def get_optuna_study_model_parameters( "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", @@ -2348,10 +2358,8 @@ def get_optuna_study_model_parameters( "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 ), } @@ -2402,11 +2410,8 @@ def get_optuna_study_model_parameters( 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", @@ -2418,17 +2423,15 @@ def get_optuna_study_model_parameters( "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 @@ -2440,16 +2443,12 @@ def get_optuna_study_model_parameters( # 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", @@ -2617,6 +2616,33 @@ def floor_to_step(value: float | int, step: int) -> int: 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, @@ -2689,28 +2715,49 @@ def get_label_defaults( *, 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( @@ -2734,4 +2781,4 @@ def get_label_defaults( 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 -- 2.53.0