From 286dba04a04370c8f7f3ef37a9cdfa843373bb57 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sat, 3 Oct 2026 18:23:41 +0200 Subject: [PATCH] fix(docs): align strategy guides and analysis reports (#285) Document effective QuickAdapter and ReforceXY behavior, supplied profiles and SDK boundaries. Clarify method-specific split defaults and standardized Yeo-Johnson labels. Align analyzer export availability and confidence-interval contracts, native template checks, evaluation and maintenance guidance. Runtime algorithms and defaults remain unchanged. Quality and CodeQL pass on the reviewed head. The automated security review is unavailable because its service quota is exceeded. --- .serena/project.yml | 5 +- CONTRIBUTING.md | 163 ++++ README.md | 753 +++--------------- ReforceXY/README.md | 402 ++++++++++ ReforceXY/reward_space_analysis/README.md | 191 ++++- .../reward_space_analysis.py | 38 +- .../tests/.docstring_template.md | 344 ++------ .../reward_space_analysis/tests/README.md | 181 +++-- .../reward_space_analysis/tests/constants.py | 18 +- .../tests/helpers/test_utilities.py | 8 +- .../integration/test_report_formatting.py | 200 ----- .../tests/statistics/test_statistics.py | 138 ++++ ReforceXY/tests/test_config_template.py | 14 + ReforceXY/tests/test_review_contracts.py | 2 +- ReforceXY/user_data/config-template.json | 34 +- ReforceXY/user_data/freqaimodels/ReforceXY.py | 8 +- docs/evaluation.md | 135 ++++ quickadapter/README.md | 349 ++++++++ quickadapter/tests/test_config_template.py | 14 + quickadapter/user_data/config-template.json | 4 +- scripts/config_template_contract.py | 42 + 21 files changed, 1749 insertions(+), 1294 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 ReforceXY/README.md delete mode 100644 ReforceXY/reward_space_analysis/tests/integration/test_report_formatting.py create mode 100644 ReforceXY/tests/test_config_template.py create mode 100644 docs/evaluation.md create mode 100644 quickadapter/README.md create mode 100644 quickadapter/tests/test_config_template.py create mode 100644 scripts/config_template_contract.py diff --git a/.serena/project.yml b/.serena/project.yml index e8939e0..4bd7d18 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -1,4 +1,3 @@ - # the encoding used by text files in the project # For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings encoding: "utf-8" @@ -139,7 +138,7 @@ ls_additional_workspace_folders: [] # - "./subproject1" # - "./subproject2" ls_workspace_folders: -- . + - . # list of language servers to start when using the LSP backend; choose from: # ada al angular ansible bash @@ -178,7 +177,7 @@ ls_workspace_folders: # The first language server is the default language and the respective language server will be used as a fallback. # Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. language_servers: -- python + - python # list of APIs (facades or facade methods, e.g. "lsp" or "lsp.get_diagnostics_for_symbol") to include in the REPL # that would otherwise be disabled (particularly optional methods, which are disabled by default). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..62b5ebe --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,163 @@ +# Contributing + +Run checks from the repository root. Use the matching Freqtrade QA image for +strategy runtime and type checks; the standalone analysis suite uses its locked +uv environment. Operator guides belong with each strategy; economic claims must +follow the [evaluation protocol](docs/evaluation.md). + +## Contents + +- [Runtime regressions](#runtime-regressions) +- [Coverage gate](#coverage-gate) +- [Quality checks](#quality-checks) + +## Runtime regressions + +Run each suite in its matching Freqtrade QA image, with the repository mounted +at `/workspace` and `/workspace` as the working directory: + +```shell +# ReforceXY +python -m unittest discover -s ReforceXY/tests -v + +# QuickAdapter +PYTHONPATH=/workspace/quickadapter/user_data/strategies \ + python -m unittest discover -s quickadapter/tests -v +``` + +Each suite's `test_config_template.py` applies the +[shared native assertion](scripts/config_template_contract.py) to its own shipped +template. It checks the unchanged default and independent/joint activation of +the two CCXT `rateLimit` examples. Only the selected comment markers are removed; +the parsed configuration must otherwise remain unchanged. Example values are +read from the template, not duplicated in the tests. + +Both commands must be run from the repository root, which is what the +container's `--workdir /workspace` provides. To select one concern, pass a +pattern that matches the whole `module.Class.method` name; a bare substring +selects more than you want, and a pattern that matches nothing runs zero tests +and fails with exit code 5: + +```shell +PYTHONPATH=quickadapter/user_data/strategies \ + python -m unittest discover -s quickadapter/tests -k 'test_utils_zigzag.*' -v +``` + +CI runs type checks and runtime regressions in one QA matrix entry per strategy. +The shared runtime step sets each strategy's `PYTHONPATH`. QuickAdapter needs +this for direct `unittest` discovery because its model imports `LabelTransformer` +and `Utils` by bare names; ReforceXY resolves its imports without it, so the +setting is optional there. QuickAdapter's regressions additionally run under +`coverage.py`; the reward-space analysis suite runs separately with `uv`, +without a Freqtrade image. + +## Coverage gate + +Within the strategy QA matrix, QuickAdapter is the only strategy whose runtime +regressions enforce a coverage gate. The standalone reward-space analysis suite +has its own gate; see [its testing documentation](ReforceXY/reward_space_analysis/tests/README.md). +QuickAdapter's configuration is `quickadapter/.coveragerc`, selected explicitly +because coverage.py looks for `.coveragerc` in the directory it is run from and +does not search parents: + +```shell +export PYTHONPATH=quickadapter/user_data/strategies +export COVERAGE_RCFILE=quickadapter/.coveragerc COVERAGE_FILE=/tmp/.coverage + +sh scripts/run-coverage.sh -s quickadapter/tests -v +``` + +The shared runner gives both coverage commands the same environment. When running +`coverage run` and `coverage report` separately, export both variables for both +commands: prefixing only the run command does not configure the later report, +which then looks in its default data path and fails with `No data to report.` + +Branch coverage distinguishes a guard's alternatives; statement coverage alone +does not prove both paths ran. `include_namespace_packages=true` enables the +discovery of completely unexecuted Python files inside namespace-package +subdirectories (directories without `__init__.py`). It does not mean imported +modules or an explicitly selected source root disappear when the flag is false. +The gate keeps it enabled so unexecuted nested files can contribute to the +denominator. + +`fail_under` is an absolute floor, never per-module. To change it, re-measure +with the shipped configuration already in place — a measurement taken without +`branch` and `include_namespace_packages` reports a different denominator and +is not a valid input — then update the value and the `# measured` annotation +above it in the same commit. Raise the floor only; a drop needs the reason in +the pull request. `quickadapter/tests/test_coverage_floor.py` refuses a +placeholder, a missing or undated measurement annotation, a measurement below +the floor it justifies, a floor below the current minimum, a floor that is not a +percentage, a `precision` coarse enough to round the total past the floor, a +disabled branch trace, a disabled namespace walk, a source tree that is not the +measured one, a `fail_under` that has drifted into the inert `[run]` section, the +source-level `no cover` or `no branch` pragmas (with or without a colon), +ellipsis-only bodies, additional `TYPE_CHECKING` blocks beyond the existing +import-only block, and any `omit`, `include`, `exclude_lines`, `exclude_also` or +`partial_*` of production code. The shared `scripts/run-coverage.sh` runner is +exercised with real passing and failing coverage reports and a failing test suite; +the checks assert process exit status, not workflow token spelling. + +## Quality checks + +Run repository quality checks from the repository root: + +Ruff does not need the Freqtrade runtime or project dependencies: + +```shell +uvx ruff@latest check . +uvx ruff@latest format --check . +``` + +BasedPyright must run inside the matching Freqtrade QA image. The repository +wrapper records the sorted repository-relative identities of every configured +Python source, requires that inventory to match BasedPyright's analyzed-file +count, and compares it with every emitted diagnostic field—including an optional +rule and source range—against the project's exact snapshot. Build each QA target +and mount the checkout read-only: + +```shell +# QuickAdapter +docker build --pull --target qa --tag freqai-strategies-quickadapter-qa quickadapter +docker run --rm \ + --mount "type=bind,src=$PWD,dst=/workspace,readonly" \ + --entrypoint python \ + freqai-strategies-quickadapter-qa \ + /workspace/scripts/check_basedpyright.py --project quickadapter + +# ReforceXY +docker build --pull --target qa --tag freqai-strategies-reforcexy-qa ReforceXY +docker run --rm \ + --mount "type=bind,src=$PWD,dst=/workspace,readonly" \ + --entrypoint python \ + freqai-strategies-reforcexy-qa \ + /workspace/scripts/check_basedpyright.py --project reforcexy +``` + +The check fails when a configured source identity or diagnostic is added, removed, +moved, or changed, or when the analyzed-file count differs from the source +inventory. Each project's direct `include` entries must be normalized, +non-overlapping relative file or directory paths; glob syntax and symbolic links +are rejected. Snapshot updates are deliberate writable operations in the matching +QA image. For example: + +```shell +docker run --rm \ + --mount "type=bind,src=$PWD,dst=/workspace" \ + --entrypoint python \ + freqai-strategies-quickadapter-qa \ + /workspace/scripts/check_basedpyright.py --project quickadapter --write +``` + +Review the generated `.basedpyright/diagnostics.json` diff. Use the ReforceXY image +and `--project reforcexy` for its snapshot. The writer preserves existing file +permissions and uses mode `0644` when creating a missing snapshot. Snapshot targets +must be regular files; symbolic links and other special files are rejected. The +wrapper rejects direct host and wrong-image execution so Freqtrade imports and +dependency versions remain exact. + +The BasedPyright and type-stub versions are pinned in each project's +`.devcontainer/requirements-dev.txt`, as is `coverage` in QuickAdapter's. +The Freqtrade base images intentionally follow their rolling `stable_freqai` +and `stable_freqairl` tags, so record the resolved image digests when a +reproducible audit is required. diff --git a/README.md b/README.md index 4cb5624..44792a6 100644 --- a/README.md +++ b/README.md @@ -1,688 +1,133 @@ # FreqAI strategies -## Table of contents - -- [QuickAdapter](#quickadapter) - - [Quick start](#quick-start) - - [Configuration tunables](#configuration-tunables) - - [Continual learning](#continual-learning) - - [Live inference](#live-inference) - - [Backtest evaluation protocol](#backtest-evaluation-protocol) -- [ReforceXY](#reforcexy) - - [Quick start](#quick-start-1) - - [Supported models](#supported-models) - - [Configuration tunables](#configuration-tunables-1) - - [Continual learning](#continual-learning-1) - - [Live inference](#live-inference-1) - - [Training and HPO](#training-and-hpo) - - [Reward and portfolio accounting](#reward-and-portfolio-accounting) -- [Development](#development) - - [Runtime regressions](#runtime-regressions) - - [Quality checks](#quality-checks) -- [Common workflows](#common-workflows) -- [Note](#note) - -## QuickAdapter - -### Quick start - -Change the timezone according to your location in -[`docker-compose.yml`](./quickadapter/docker-compose.yml). - -From the repository root, configure, build and start the QuickAdapter container: - -```shell -cd quickadapter -cp user_data/config-template.json user_data/config.json -``` - -Adapt the configuration to your needs: edit `user_data/config.json` to set your -exchange API keys and tune the `freqai` section. - -The API server is disabled by default. Before enabling it, replace its username, -password, JWT secret and WebSocket token: the template ships public placeholders -that provide no protection until changed. Keep the Compose port bound to -localhost unless access is protected by a VPN or SSH tunnel. - -Then build and start the container: - -```shell -docker compose up -d --build -``` - -The build intentionally follows Freqtrade's current `stable_freqai` image and -resolves some dependencies at build time. Record the resolved image digest and -dependency versions for reproducible evaluations, as required by the protocol -below. - -### Configuration tunables - -| Path | Runtime fallback | 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 Freqtrade's [`fit_live_predictions_candles`][freqai-parameters] 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_natr_method | `moving_average` | enum {`moving_average`,`quantile_interpolation`,`weighted_average`} | Trade NATR (Normalized Average True Range) aggregation for stoploss and take-profit distances. `moving_average` uses KAMA to preserve nonnegative volatility. The stoploss never loosens, including after partial exits, and remains unchanged on order fills. | -| exit_pricing.final_take_profit_retracement_fraction | 0.25 | float (0,1] | Fraction of the final take-profit target distance used as the frozen trailing retracement distance after the final target arms the exit. The final exit tracks the best subsequent per-candle rate and exits only after this material adverse move; elapsed stagnation alone does not exit. Plot annotations show only the current trail boundary from the candle that established it; earlier boundaries are not retained. | -| _Reversal confirmation_ | | | | -| reversal_confirmation.lookback_period_candles | 0 | int >= 0 | Prior confirming candles; 0 = none. With confirmation enabled, unmeasurable history rejects entries, while a valid current exit may still reduce exposure. | -| reversal_confirmation.decay_fraction | 0.5 | float (0,1] | Geometric per-candle volatility adjusted reversal threshold relaxation factor. | -| reversal_confirmation.min_natr_multiplier_fraction | 0.0095 | float [0,1] | Lower bound fraction (< upper bound) for volatility adjusted reversal threshold. | -| reversal_confirmation.max_natr_multiplier_fraction | 0.0125 | float [0,1] | Upper bound fraction (> lower bound) for volatility adjusted reversal threshold. | -| _Regressor model_ | | | | -| freqai.regressor | `xgboost` | enum {`xgboost`,`lightgbm`,`histgradientboostingregressor`,`ngboost`,`catboost`} | Machine learning regressor. With validation and early stopping enabled, NGBoost uses the iteration with the best validation score for prediction and to determine the final refit size. | -| freqai.continual_learning | false | bool | Continue training the deployed XGBoost, LightGBM or CPU CatBoost model. GPU CatBoost and other regressors train from scratch. With two-stage selection (see `test_size`), HPO and selection train from scratch; only the final refit continues the deployed model, adding the selected number of boosting rounds. Continued training reuses the saved feature and label transformations, including after restart. Missing or incompatible transformations or deployment metadata prevent continuation. Reset trained models or use a new `freqai.identifier` to change pipeline settings or recover from incompatible saved state. | -| _Model training parameters_ | | | | -| freqai.model_training_parameters.gpu_vram_gb | 80 | int > 0 | Available GPU VRAM (GB) for CatBoost, not total. Any positive value is floored to the nearest supported tier `<= value` (tiers 8, 10, 12, 16, 24, 32, 40, 48, 64, 80; values below 8 use tier 8). Constrains `depth`, `border_count`, and `max_ctr_complexity` ranges. | -| _Data split parameters_ | | | | -| freqai.data_split_parameters.method | `train_test_split` | enum {`train_test_split`,`timeseries_split`} | Data splitting strategy. `train_test_split` for sequential split, `timeseries_split` for chronological split with configurable gap. | -| freqai.data_split_parameters.test_size | 0.1 | float [0,1) \| int >= 0 \| null | Outer holdout size; `0` disables the holdout (single-stage fit, `train_test_split` only). The same parameter reserves the chronological tail of the remaining training rows as inner validation for HPO and early stopping; a fractional value is relative to those remaining rows, not the original window. The holdout is predicted once and reported as weighted `holdout_rmse` in the original label scale; it measures the cold-started pre-refit selection model, not the refitted deployed model. `null` (sklearn dynamic sizing) applies only to `timeseries_split`; `train_test_split` requires a float or int; inner validation then falls back to `0.1`. | -| freqai.data_split_parameters.n_splits | 5 | int >= 2 | Controls train/test proportions for `timeseries_split` (higher = larger train set). | -| freqai.data_split_parameters.gap | 0 | int >= 0 | Samples to exclude between train/test for `timeseries_split`. `0` auto-derives the gap (source and lower-bound rule depend on `causal_mode`; see `causal_mode`). Not used by `train_test_split`. | -| freqai.data_split_parameters.max_train_size | null | int >= 1 \| null | Maximum training set size for `timeseries_split`. When set, creates a sliding window instead of expanding train set. `null` = no limit. | -| _Label smoothing_ | | | | -| freqai.label_smoothing.method | `gaussian` | enum {`none`,`gaussian`,`kaiser`,`kaiser_bessel_derived`,`triang`,`smm`,`sma`,`savgol`,`gaussian_filter1d`} | Label smoothing method (`smm`=median, `sma`=mean, `savgol`=Savitzky–Golay). | -| freqai.label_smoothing.window_candles | 5 | int >= 1 | Requested smoothing window in candles. Runtime raises values below 3; Gaussian, Kaiser, triangular, SMM and SMA use the next odd length, `kaiser_bessel_derived` uses the next even length, and `savgol` uses an odd length greater than `polyorder`. `none` does not smooth. For `gaussian_filter1d`, this value only gates series shorter than the requested window; `sigma` defines the kernel. | -| freqai.label_smoothing.beta | 8.0 | float > 0 | Shape parameter for `kaiser` and `kaiser_bessel_derived` kernels. | -| freqai.label_smoothing.polyorder | 3 | int >= 0 | Polynomial order for `savgol` smoothing. | -| freqai.label_smoothing.mode | `mirror` | `savgol`: enum {`mirror`,`constant`,`nearest`,`wrap`,`interp`}; `gaussian_filter1d`: enum {`mirror`,`constant`,`nearest`,`wrap`} | Boundary mode for `savgol` and `gaussian_filter1d`; ignored otherwise. | -| freqai.label_smoothing.sigma | 1.0 | float > 0 | Gaussian `sigma` for `gaussian_filter1d` smoothing. | -| _Label weighting_ | | | | -| freqai.label_weighting.strategy | `none` | enum {`none`,`uniform`,`amplitude`,`amplitude_threshold_ratio`,`volume_rate`,`speed`,`efficiency_ratio`,`volume_weighted_efficiency_ratio`,`combined`} | Label weighting metric: none (`none`), uniform unit weight on every detected pivot (`uniform`), swing amplitude (`amplitude`), swing amplitude / median volatility-threshold ratio (`amplitude_threshold_ratio`), swing volume per candle (`volume_rate`), swing speed (`speed`), swing efficiency ratio (`efficiency_ratio`), swing volume-weighted efficiency ratio (`volume_weighted_efficiency_ratio`), or combined metrics aggregation (`combined`). Switching between `none` and any other strategy requires deleting trained models to realign training emphasis. | -| freqai.label_weighting.metric_coefficients | {} | dict[str, finite float > 0] | Per-metric coefficients for `combined` strategy. Keys: `amplitude`, `amplitude_threshold_ratio`, `volume_rate`, `speed`, `efficiency_ratio`, `volume_weighted_efficiency_ratio`. Invalid entries are ignored; when none remain, all metrics are selected with coefficient `1.0`. | -| freqai.label_weighting.aggregation | `arithmetic_mean` | enum {`arithmetic_mean`,`geometric_mean`,`harmonic_mean`,`quadratic_mean`,`weighted_median`,`softmax`} | Metric aggregation method for `combined` strategy. `arithmetic_mean`=(Σ(w·m)/Σ(w)), `geometric_mean`=(∏(m^w))^(1/Σw), `harmonic_mean`=Σ(w)/(Σ(w/m)), `quadratic_mean`=(Σ(w·m²)/Σ(w))^(1/2), `weighted_median`=Q₀.₅(m,w), `softmax`=Σ(m·s_i) where s_i=w_i·exp(m_i/T)/Σ(w_j·exp(m_j/T)). | -| freqai.label_weighting.softmax_temperature | 1.0 | float > 0 | Temperature T for `softmax` aggregation, controls distribution sharpness. | -| freqai.label_weighting.fill_method | `zero` | enum {`zero`,`epsilon`,`gaussian`,`epsilon_gaussian`} | Off-pivot weighting scheme. `zero` hard-zeros off-pivot rows; `epsilon` applies the epsilon floor `fill_epsilon * (pivot_weights)`; `gaussian` applies per-pivot Gaussian bumps; `epsilon_gaussian` sums the `epsilon` floor and the `gaussian` bumps. Pivot rows take the max of their raw weight and the off-pivot field at their index (no-op for `zero`). Under `causal_mode=true` the epsilon baseline is computed causally (see `causal_mode`). Switching away from `zero` may require retuning tree-leaf regularization (`min_child_weight`, `lambda`) and resetting any prior Optuna study. Changing this parameter requires deleting trained models. | -| freqai.label_weighting.fill_epsilon | 0.000001 | float [0,1] | Off-pivot fraction of the pivot baseline. Ignored when `fill_method` not in {`epsilon`,`epsilon_gaussian`}. | -| freqai.label_weighting.fill_epsilon_baseline | `mean` | enum {`mean`,`median`} | Pivot baseline statistic. `mean` tracks central tendency; `median` is robust against pivot-weight skew. Ignored when `fill_method` not in {`epsilon`,`epsilon_gaussian`}. | -| freqai.label_weighting.fill_sigma_candles | 25.0 | float >= 0.5 | Gaussian standard deviation in candles for the per-pivot bumps. Acts as the upper bound on per-pivot sigma when `fill_bandwidth == "knn"`. Lower bound 0.5 prevents severe underflow in the Gaussian tail. Under `causal_mode=true` the bumps use a finite support `ceil(4 * fill_sigma_candles)` (see `causal_mode`). Ignored when `fill_method` not in {`gaussian`,`epsilon_gaussian`}. Gaussian weight availability also waits for resolution of possible pivots throughout that support; pure-Gaussian uniform pivot centers retain their own confirmation time. | -| freqai.label_weighting.fill_sigma_min_candles | 0.5 | float >= 0.5 | Lower bound on per-pivot sigma in candles when `fill_bandwidth == "knn"`. Clipped to `fill_sigma_candles` when larger. Ignored when `fill_method` not in {`gaussian`,`epsilon_gaussian`} or `fill_bandwidth != "knn"`. | -| freqai.label_weighting.fill_bandwidth | `fixed` | enum {`fixed`,`knn`} | Per-pivot Gaussian bandwidth selector. `fixed` applies a constant `fill_sigma_candles` to every pivot. `knn` adapts each pivot's sigma to local pivot density via `sigma_p = clip(fill_bandwidth_alpha * d_k(p), fill_sigma_min_candles, fill_sigma_candles)` where `d_k(p)` is the index distance to the `k`-th nearest pivot neighbor ([Loftsgaarden and Quesenberry][knn-density]; [Silverman, §5.2][silverman-density]). Mitigates the crushing of weaker pivots by stronger neighbors in dense clusters. Ignored when `fill_method` not in {`gaussian`,`epsilon_gaussian`}. | -| freqai.label_weighting.fill_bandwidth_neighbors | 1 | int >= 1 | `k` for the k-nearest-neighbor bandwidth selector. Ignored when `fill_method` not in {`gaussian`,`epsilon_gaussian`} or `fill_bandwidth != "knn"`. | -| freqai.label_weighting.fill_bandwidth_alpha | 0.5 | float > 0 | Multiplicative factor on the k-th neighbor distance. Smaller values produce sharper, more separated Gaussians; larger values approach the `fixed` behavior. Ignored when `fill_method` not in {`gaussian`,`epsilon_gaussian`} or `fill_bandwidth != "knn"`. | -| freqai.label_weighting.support_policy | `fallback` | enum {`fallback`,`raise`} | Policy when active label weighting fails support checks (evaluated on the training rows surviving upstream filtering). `raise` aborts the fit; `fallback` logs a `WARNING` and uses sanitized base sample weights for that fit. Eval (test/val) weights bypass this policy and fall back only when label support collapses; shape or alignment errors remain fatal. | -| freqai.label_weighting.min_pivot_equivalent_count | 3 | int >= 1 | Minimum number of surviving pivot-equivalent label weights required after filtering. Pivot-equivalent rows are weights at least 10% of the surviving maximum label weight. | -| freqai.label_weighting.min_positive_label_weight_fraction | 0.01 | float [0,1] | Minimum fraction of filtered training rows with finite positive label weights. | -| freqai.label_weighting.min_effective_sample_size | 3.0 | float >= 1 | Minimum Kish effective sample size of the final composed training weights. | -| _Label pipeline_ | | | | -| freqai.label_pipeline.standardization | `none` | enum {`none`,`zscore`,`robust`,`mmad`,`power_yj`} | Standardization method applied to labels before normalization. `none`=w, `zscore`=(w-μ)/σ, `robust`=(w-median)/(Q₃-Q₁), `mmad`=(w-median)/(MAD·k), `power_yj`=YJ(w). | -| freqai.label_pipeline.robust_quantiles | [0.25, 0.75] | list[float] where 0 <= Q1 < Q3 <= 1 | Quantile range for robust standardization, Q1 and Q3. | -| freqai.label_pipeline.mmad_scaling_factor | 1.4826 | float > 0 | Scaling factor for MMAD standardization. | -| freqai.label_pipeline.normalization | `maxabs` | enum {`maxabs`,`minmax`,`sigmoid`,`none`} | Normalization method applied to labels. `maxabs`=w/max(\|w\|), `minmax`=low+(w-min)/(max-min)·(high-low), `sigmoid`=2·σ(scale·w)-1, `none`=w. | -| freqai.label_pipeline.minmax_range | [-1.0, 1.0] | list[float], low < high | Target range for `minmax` normalization, min and max. | -| freqai.label_pipeline.sigmoid_scale | 1.0 | float > 0 | Scale parameter for `sigmoid` normalization, controls steepness. | -| freqai.label_pipeline.gamma | 1.0 | float (0,10] | Contrast exponent applied to labels after normalization: >1 emphasizes extrema, values between 0 and 1 soften. | -| _Feature parameters_ | | | | -| freqai.feature_parameters.label_period_candles | min/max midpoint | int >= 1 | Zigzag labeling NATR period. | -| freqai.feature_parameters.label_horizon_candles | `label_period_candles` | int >= 1 | Conservative fixed purge horizon in candles: the magnitude of the causal guards' purge and of the default `timeseries_split` gap (see `causal_mode` for how the guards consume it). When unset, falls back to `label_period_candles`. | -| freqai.feature_parameters.causal_mode | true | bool | Causal split-guard master toggle. When `true` (default): (1) rejects `data_split_parameters.shuffle=true`, `feature_parameters.shuffle_after_split=true`, and `feature_parameters.reverse_train_test_order=true` (two of these rejections are independent of this toggle: `timeseries_split` rejects `shuffle_after_split` structurally, and an active holdout `test_size != 0` rejects all three at evaluation); (2) for `timeseries_split`, auto-sets `gap=label_horizon_candles` when `gap` is unset or `0` and rejects an explicit `gap= 1 | Minimum labeling NATR period used for reversals labeling HPO. | -| freqai.feature_parameters.max_label_period_candles | 24 | int >= 1 | Maximum labeling NATR period used for reversals labeling HPO. | -| freqai.feature_parameters.label_natr_multiplier | min/max midpoint | float > 0 | Zigzag labeling NATR multiplier. | -| freqai.feature_parameters.min_label_natr_multiplier | 9.0 | float > 0 | Minimum labeling NATR multiplier used for reversals labeling HPO. | -| freqai.feature_parameters.max_label_natr_multiplier | 12.0 | float > 0 | Maximum labeling NATR multiplier used for reversals labeling HPO. | -| freqai.feature_parameters.label_frequency_candles | `auto` | int [2, 10000] \| `auto` | Reversals labeling frequency. `auto` = max(2, 2 \* number of whitelisted pairs). | -| freqai.feature_parameters.label_weights | uniform | list of 7 finite floats >= 0; sum > 0 | Per-objective weights for trial selection methods, normalized internally. Objectives: (1) number of detected reversals, (2) median swing amplitude, (3) median (swing amplitude / median volatility-threshold ratio), (4) median swing ratio of mean volume per candle to median volume (dimensionless), (5) median swing speed, (6) median swing efficiency ratio, (7) median swing volume-weighted efficiency ratio. Zero-weight objectives do not contribute to power means, including negative and zero orders; positive-weight zero values retain their usual power-mean semantics. Clustering excludes zero-weight dimensions before estimating the cluster count and forming clusters; positive weights score clusters and candidates without rescaling cluster coordinates. Weights are validated before constant-objective projection; an all-zero projected vector falls back to uniform weights. | -| freqai.feature_parameters.label_p_order | null | `minkowski`: finite float > 0; `power_mean`: finite float; null otherwise | Lp exponent for parameterized distance metrics. Used by `minkowski` distance (default 2.0) and `power_mean` distance (default 1.0). The KNN `power_mean` aggregation exponent is configured by `label_density_aggregation_param`. Ignored by other metrics. | -| freqai.feature_parameters.label_method | `compromise_programming` | enum {`compromise_programming`,`topsis`,`kmeans`,`kmeans2`,`knn`,`medoid`} | HPO `label` Pareto front trial selection method. `kmedoids` is unavailable in the current Python 3.14 image. | -| freqai.feature_parameters.label_distance_metric | `euclidean` | enum {`euclidean`,`minkowski`,`chebyshev`,`cityblock`,`sqeuclidean`,`seuclidean`,`mahalanobis`,`harmonic_mean`,`geometric_mean`,`arithmetic_mean`,`quadratic_mean`,`cubic_mean`,`power_mean`,`weighted_sum`} | Distance metric for `compromise_programming` and `topsis` methods. Invalid values warn and fall back to `euclidean`. | -| freqai.feature_parameters.label_cluster_metric | `euclidean` | enum {`euclidean`,`minkowski`,`chebyshev`,`cityblock`,`sqeuclidean`,`seuclidean`,`mahalanobis`} | Distance metric for `kmeans` and `kmeans2`. Invalid values warn and fall back to `euclidean`. | -| freqai.feature_parameters.label_cluster_selection_method | `topsis` | enum {`compromise_programming`,`topsis`} | Cluster selection method for clustering-based label methods. | -| freqai.feature_parameters.label_cluster_trial_selection_method | `topsis` | enum {`compromise_programming`,`topsis`} | Best cluster trial selection method for clustering-based label methods. | -| freqai.feature_parameters.label_density_metric | method-dependent | enum {`euclidean`,`minkowski`,`chebyshev`,`cityblock`,`sqeuclidean`,`seuclidean`,`mahalanobis`} | Distance metric for `knn` and `medoid` methods. Invalid values warn and fall back to the method's natural default (`minkowski` for `knn`, `euclidean` for `medoid`). | -| freqai.feature_parameters.label_density_aggregation | `power_mean` | enum {`power_mean`,`quantile`,`min`,`max`} | Aggregation method for KNN neighbor distances. | -| freqai.feature_parameters.label_density_n_neighbors | 5 | int >= 1 | Number of neighbors for KNN. | -| freqai.feature_parameters.label_density_aggregation_param | aggregation-dependent | `power_mean`: finite float; `quantile`: float [0,1]; null otherwise | Tunable for KNN neighbor distance aggregation: Lp exponent (`power_mean`) or quantile value (`quantile`). | -| freqai.feature_parameters.scaler | `minmax` | enum {`minmax`,`maxabs`,`standard`,`robust`} | Feature scaling method. `minmax`=MinMaxScaler, `maxabs`=MaxAbsScaler, `standard`=StandardScaler, `robust`=RobustScaler. Changing this parameter requires deleting trained models. | -| freqai.feature_parameters.range | [-1.0, 1.0] | list[float], low < high | Target range for `minmax` scaler, min and max. Changing this parameter requires deleting trained models. | -| _Label prediction_ | | | | -| freqai.label_prediction.method | `thresholding` | enum {`none`,`thresholding`} | Prediction method. `none` disables threshold computation; `thresholding` enables adaptive thresholds. See the calibration warmup rules below. | -| freqai.label_prediction.selection_method | `rank_extrema` | enum {`rank_extrema`,`rank_peaks`,`partition`} | Extrema selection method. `rank_extrema` ranks extrema values, `rank_peaks` ranks detected peak values, `partition` uses sign-based partitioning. | -| freqai.label_prediction.threshold_method | `mean` | enum {`mean`,`isodata`,`li`,`minimum`,`otsu`,`triangle`,`yen`,`median`,`soft_extremum`} | Thresholding method for prediction thresholds. | -| freqai.label_prediction.soft_extremum_alpha | 12.0 | float >= 0 | Alpha for `soft_extremum` threshold method. | -| freqai.label_prediction.outlier_quantile | 0.999 | float (0,1) | Quantile threshold for predictions outlier filtering. | -| freqai.label_prediction.keep_fraction | 0.0075 | float (0,1] | Fraction of extrema used for thresholds. 1 uses all, lower values keep only most significant. Applies to `rank_extrema` and `rank_peaks`; ignored for `partition`. | -| _Optuna / HPO_ | | | | -| freqai.optuna_hyperopt.enabled | false | bool | Enables regressor and dynamic label HPO. | -| freqai.optuna_hyperopt.sampler | `tpe` | enum {`tpe`,`auto`} | HPO sampler algorithm for `hp` namespace. `tpe` uses [TPESampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.TPESampler.html) with multivariate, group, and constant_liar (when multiple workers), `auto` uses [AutoSampler](https://hub.optuna.org/samplers/auto_sampler). | -| freqai.optuna_hyperopt.label_sampler | `auto` | enum {`auto`,`tpe`,`nsgaii`,`nsgaiii`} | HPO sampler algorithm for multi-objective `label` namespace. `nsgaii` uses [NSGAIISampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.NSGAIISampler.html), `nsgaiii` uses [NSGAIIISampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.NSGAIIISampler.html). | -| freqai.optuna_hyperopt.storage | `file` | enum {`file`,`sqlite`} | HPO storage backend. | -| freqai.optuna_hyperopt.continuous | true | bool | Continuous HPO. Forced for both namespaces in backtest and hyperopt, resetting the study on each optimization. | -| freqai.optuna_hyperopt.warm_start | true | bool | Warm start HPO with previous best value(s). Persisted values are loaded and saved only in live and dry-run modes; non-live runs reuse only values produced earlier in the same run. | -| freqai.optuna_hyperopt.n_startup_trials | 15 | int >= 0 | HPO startup trials. | -| freqai.optuna_hyperopt.n_trials | 50 | int >= 1 | Maximum HPO trials. | -| freqai.optuna_hyperopt.n_jobs | 1 | int >= 1 | Parallel HPO workers. | -| freqai.optuna_hyperopt.timeout | 7200 | int >= 0 | HPO wall-clock timeout in seconds. | -| freqai.optuna_hyperopt.label_candles_step | 1 | int >= 1 | Step for Zigzag NATR period `label` search space. | -| freqai.optuna_hyperopt.space_reduction | false | bool | Enable/disable `hp` search space reduction based on previous best parameters. | -| freqai.optuna_hyperopt.space_fraction | 0.4 | float [0,1] | Fraction of the `hp` search space to use with `space_reduction`. Lower values create narrower search ranges around the best parameters. | -| freqai.optuna_hyperopt.min_resource | 3 | int >= 1 | Minimum resource per [HyperbandPruner](https://optuna.readthedocs.io/en/stable/reference/generated/optuna.pruners.HyperbandPruner.html) rung. | -| freqai.optuna_hyperopt.seed | 1 | int [0, 4294967295] | HPO RNG seed used by the Optuna samplers and label-candle shuffling. | -| freqai.optuna_hyperopt.reset_label_study_on_schema_mismatch | true | bool | Reset a persisted `label` study when its selection schema is missing, invalid, or incompatible. `true` performs a destructive reset, deleting the study before recreating it; `false` preserves its trials and stored metadata, permits caller-managed reuse in memory, and does not persist selected params until the schema is reconciled. Both fail closed: an inspection error, or (under `true`) a deletion error, aborts study creation. Has no effect when `continuous=true` or outside live/dry-run modes, where studies are always reset. | -| freqai.optuna_hyperopt.vary_model_seed_by_trial | true | bool | Add `trial.number` to each regressor's configured model seed (or its default seed of `1`) during HPO. `true` samples model randomness across trials; `false` evaluates every trial and the final fit with the same model seed. This does not change `freqai.optuna_hyperopt.seed`. | - -The `label_weighting`, `label_smoothing`, `label_pipeline` and -`label_prediction` sections accept either the flat paths listed above or a -per-label format using `default` and `columns.`. Do not mix both formats in -one section: once `default` or `columns` is present, sibling flat keys are -ignored with a warning. Matching column patterns are applied from least to most -specific; equally specific patterns follow declaration order, so the later one -wins. - -MMAD standardization preserves finite constant columns: with `none` or `maxabs` -normalization they transform to zero and invert exactly, including near-limit -and subnormal floats. - -### Continual learning - -In backtests, continual training uses only a saved model whose training cutoff -precedes the current window's end and its last available candle boundary. A -later model left under the same identifier is not reused when a backtest is -extended into the past. Live and dry-run restarts still restore compatible -deployed models. - -### Live inference - -In live and dry-run modes, each pair requires -`freqai.fit_live_predictions_candles` real model predictions before adaptive -thresholds become available. The Nth observation first affects the next -prediction update, not the candle that produced it. FreqAI bootstrap -predictions made from the initial training frame do not count. Warmup progress -from persisted real predictions is restored after a restart. FreqAI saves -prediction history after training attempts and on clean shutdown. After an -abrupt stop or hard reboot, predictions since the last history save may be -lost, so those observations must accumulate again. Legacy rows missing -provenance, including rows in partly marked histories, count only when their -nonzero, nonexpired prediction status distinguishes them from bootstrap; -ambiguous rejected rows and explicit false markers do not count. - -A pair starts a new warmup when the time since its last observation, or a gap -within its observations, is greater than -`fit_live_predictions_candles × timeframe`. An observation exactly one horizon -old remains eligible. Downtime and expired-model rows are excluded; genuine -zero and outlier-rejected predictions count. Predictions align to candle dates; -candles without predictions have `do_predict=0` and downtime zeros display but -never calibrate. - -After a restart, persisted `holdout_rmse` is recovered from the latest produced -prediction available at the decision time, independently of the calibration -window. A newer unavailable score remains unavailable rather than reviving an -older finite score. Recovery does not warm up adaptive thresholds; a model fitted -in the current session keeps its in-memory score. - -### Backtest evaluation protocol - -Evaluate a proposed change against the current configuration on the same unseen -market history. Judge portfolio performance after costs, not training loss. This -procedure does not establish that the current defaults are optimal. - -#### What the backtest measures - -In Freqtrade 2026.8, the [native backtest][freqai-running] constructs each pair's -rolling predictions before replaying enabled `fit_live_predictions()` updates -([training loop][freqai-source], [replay loop][freqai-replay]). It exercises -rolling model fits, threshold replay and strategy decisions, but a label-HPO -update during replay cannot affect an already-trained model. Testing that live -feedback requires a chronological runner that interleaves training, prediction -and state updates, or a forward dry-run. This repository provides no such runner; -do not present native-backtest results as validation of the complete live loop. - -QuickAdapter predicts smoothed Zigzag morphology, not returns. `holdout_rmse` -measures the selection model's weighted error on the original label scale, -on a holdout within the training window, before any deployment refit. An empty -holdout, including one emptied by causal purging, yields `holdout_rmse=inf` -(unavailable). With `method=train_test_split`, `test_size=0` disables internal -validation and final refit, not later rolling predictions; `timeseries_split` -does not accept zero. Use RMSE to diagnose prediction quality, not profitability. - -#### Design the comparison - -1. **Fix the question before inspecting results.** Specify the incumbent, - candidate change, pair universe, evaluation dates, training/prediction window - lengths, HPO budget, seeds and costs. Choose a primary economic metric, a - minimum worthwhile improvement and acceptable risk limits. Record all tried - configurations, including failures. Reserve a final chronological period for - confirmation; once used to revise the strategy, it is no longer unseen. -2. **Reproduce the information available at each decision.** Train on earlier - data and compare both configurations on identical subsequent timestamps. - Account for listing/delisting dates and missing candles; selecting only - today's surviving pairs biases historical results. Fit preprocessing and - select features/model hyperparameters inside each training window, using - time-ordered inner validation. Keep scoring windows outside model selection. - Threshold calibration must use only predictions available at that time. -3. **Respect label availability.** Keep `causal_mode` enabled. A historical row - is not usable for training until all observations needed for its labels and - weights are known. Add each `known_at_lookahead` candle offset to its row - position in the unsliced window; use the latest availability across labels - and weights. Audit it against each split cutoff, rejecting unknown or - out-of-frame availability. `causal_mode` alone is not proof of this invariant. - Allow for additional publication/execution delays where relevant. Purging - removes overlapping label information; an embargo excludes training samples - immediately _after_ a validation block when a split uses future training data - ([López de Prado][afml]). Prefer earlier-only training here, not an arbitrary - universal embargo duration. -4. **Isolate the change and its state.** Start with fixed label/model parameters - when comparing a component; evaluate tuning separately if it is part of the - proposed behavior. Dynamic label HPO optimizes morphology in - `fit_live_predictions()`, not held-out trading returns: judge its choices on - subsequent economic results using the live-loop evaluation above. With - validation enabled, QuickAdapter cold-starts regressor trials and the - selection model; inherited models are reserved for deployment refit. Use separate - `freqai.identifier` values and model, prediction and Optuna storage for each - configuration/seed. `--cache none` bypasses backtest-result caching, not FreqAI - model or prediction reuse. - -#### Measure economics and uncertainty - -- **Model costs and execution.** Hold sizing, protections and execution rules - constant unless they are the change under test. Set `--fee` explicitly, use - `--enable-protections` when evaluating protections, and use downloaded detail - candles with `--timeframe-detail` where feasible. Compare plausible base and - adverse cost scenarios, including spread, slippage, impact and funding/borrow - costs where applicable. Freqtrade's [candle assumptions][freqtrade-backtesting] - do not establish realistic fills or capacity; non-fee execution effects need - a separate model. A dry-run checks forward behavior, not actual exchange fills. -- **Report portfolio outcomes.** Compare net return, maximum drawdown, exposure, - turnover and trade count, with results by period and long/short side. State the - equity convention and sampling interval. Closed-trade balance omits unrealized - losses: use equity including open positions for portfolio drawdown, or label - the reported balance-based measure and its limitation. Do not average window - drawdowns. Report prediction coverage, failed windows, `holdout_rmse` and - training latency alongside economics. Do not discard failed runs to improve - averages. Cash/buy-and-hold provide context, not a replacement for the incumbent. -- **Separate market uncertainty from training randomness.** Repeat stochastic - fits/searches with the same planned seed list for both configurations and - report the paired differences, not just the best run. Seeds reuse the same - market history; they are not independent market samples. There is no universal - sufficient seed count. Record sampler/model seeds and parallelism; a fixed - seed alone does not guarantee identical HPO or GPU results. -- **Match inference to the data.** For uncertainty in mean performance, compare - aligned portfolio returns at a stated frequency. A paired block bootstrap can - preserve temporal dependence by resampling the same time blocks for both - configurations ([Politis and Romano][stationary-bootstrap]). State the effect, - interval method, confidence level, block-length choice and sensitivity to it. - Justify the dependence/stationarity assumptions; neither extra seeds nor more - bootstrap draws compensate for short history or regime changes. Maximum - drawdown is path-dependent: an interval for mean return is not its risk bound. - Report results as inconclusive when the data cannot support the intended claim. -- **Account for strategy selection.** Repeatedly choosing the best backtest - inflates apparent performance ([Bailey et al.][pbo]). If making significance - claims across candidates, define the comparison family and use valid - dependence-aware tests with a multiple-testing correction such as - [Holm's procedure][holm]; correction cannot repair invalid underlying p-values. - Report effect sizes and uncertainty, not only significance. Keep drawdown and - cost sensitivity visible rather than reducing the decision to a single score. - -#### Confirm and preserve the evidence - -Run [lookahead analysis][lookahead-analysis] and [recursive -analysis][recursive-analysis] to investigate leakage and startup sensitivity. -Use adequate history for every informative timeframe and a separate disposable -FreqAI identifier for each analysis, with no existing model directory. **Both -commands delete the selected identifier's model directory during analysis.** -Never use retained or live-run identifiers. Exempt only confirmed -target-construction flags; investigate feature and signal differences. Clean -results cover only the paths exercised, not the absence of all leakage. - -Evaluate the frozen candidate on the reserved period, then check forward behavior -in dry-run. Adopt it only if the evidence supports the planned economic and risk -criteria; otherwise retain the incumbent and distinguish rejection from -insufficient evidence. Archive a timestamped run manifest with commits, resolved -image/dependency versions, configuration/data hashes, commands, identifiers, -seeds, HPO histories, split cutoffs, costs and results. The Docker base tag moves; -record the image digest, not just `stable_freqai`. - -[afml]: https://www.wiley.com/en-us/Advances+in+Financial+Machine+Learning-p-9781119482086 -[freqai-parameters]: https://www.freqtrade.io/en/stable/freqai-parameter-table/#general-configuration-parameters -[freqai-running]: https://www.freqtrade.io/en/stable/freqai-running/ -[freqai-replay]: https://github.com/freqtrade/freqtrade/blob/2026.8/freqtrade/freqai/freqai_interface.py#L895-L935 -[freqai-source]: https://github.com/freqtrade/freqtrade/blob/2026.8/freqtrade/freqai/freqai_interface.py#L272-L409 -[freqtrade-backtesting]: https://www.freqtrade.io/en/stable/backtesting/ -[holm]: https://www.jstor.org/stable/4615733 -[knn-density]: https://doi.org/10.1214/aoms/1177700079 -[lookahead-analysis]: https://www.freqtrade.io/en/stable/lookahead-analysis/ -[pbo]: https://doi.org/10.21314/JCF.2016.322 -[recursive-analysis]: https://www.freqtrade.io/en/stable/recursive-analysis/ -[silverman-density]: https://doi.org/10.1201/9781315140919 -[stationary-bootstrap]: https://doi.org/10.1080/01621459.1994.10476870 - -## ReforceXY - -### Quick start - -Change the timezone according to your location in -[`docker-compose.yml`](./ReforceXY/docker-compose.yml). - -From the repository root, configure, build and start the ReforceXY container: - -```shell -cd ReforceXY -cp user_data/config-template.json user_data/config.json -``` - -Adapt the configuration to your needs: edit `user_data/config.json` to set your -exchange API keys and tune the `freqai` section. - -Then build and start the container: - -```shell -docker compose up -d --build -``` - -### Supported models - -PPO, MaskablePPO, RecurrentPPO, DQN, QRDQN - -### Configuration tunables - -The documented list of model tunables is at the top of the -[ReforceXY.py](./ReforceXY/user_data/freqaimodels/ReforceXY.py) file. - -`RLAgentStrategy` clamps configured leverage to `[1, max_leverage]`. An omitted, -non-finite, non-representable, Boolean or non-numeric value falls back to -Freqtrade's `proposed_leverage` before clamping. - -### Continual learning - -Continual learning trains an independent copy of the deployed policy with its -fitted feature pipeline. DQN/QRDQN deployments each persist their replay buffer; -it is loaded only when continual training starts. Missing or incompatible replay -data prevents continuation but does not prevent inference. Reset trained models -or use a new `freqai.identifier` to migrate incompatible artifacts, including -deployments without the chronological training marker. Training disables -`shuffle_after_split`. HPO studies and saved best parameters are reused only -when their objective identity matches. - -Backtests continue only from a saved policy whose training cutoff precedes -the current window's end and its last available candle boundary. A later -deployment under the same identifier is not reused for an earlier window, -even when FreqAI has saved only metadata for intervening windows. Live and -dry-run restarts still restore compatible deployed policies. - -### Live inference - -Optional `fit_live_predictions_candles` statistics use the latest persisted real -predictions per pair, excluding FreqAI bootstrap rows. Available observations -are used before a full window accumulates and survive restarts. FreqAI returns -the initial strategy frame before calculating live statistics; restored -statistics appear on the next prediction update. Legacy rows missing provenance, -including rows in partly marked histories, can count when their nonzero, -nonexpired prediction status distinguishes them from bootstrap; zero-status rows -remain excluded because bootstrap and rejected predictions cannot be -distinguished. Explicit false markers remain excluded. -On duplicate candle dates, a provable prediction takes precedence over an -ambiguous close-bearing legacy row during history restoration. -Rows with an invalid `date_pred` are discarded with a per-pair warning and the -discarded-row count; valid duplicates retain the same precedence. -See the model docstrings for continuation, HPO and statistics details. - -With `hold_potential_enabled=true`, ReforceXY enables `add_state_info` before -constructing environments so training and inference use the same observations. -Freqtrade does not support these state observations in backtesting; disable hold -potential and state observations for backtests. Live action masks use the real -open position even when state observations are disabled. Live frame stacks and -recurrent states persist per pair and model only across adjacent candles. Gaps, -repeated candles and model replacement start a new sequence; historical live -position features are not reconstructed. Prediction validity covers every source -row in the observation and every retained frame, not only the final candle. - -### Training and HPO - -Optuna HPO trains fresh candidates with the selected parameters. With continual -learning, the final fit resumes the deployed model in its frozen feature -coordinates, including its discount factor. Otherwise, it trains a fresh model -with the selected parameters. An explicitly sampled `target_kl=null` disables -the KL stopping threshold even when `model_training_parameters` specifies a -numeric value. Environment prices remain raw regardless of -`drop_ohlc_from_features`. Training returns the best checkpoint saved by the -current evaluation run when available. DQN/QRDQN HPO rejects warmup budgets that -leave no gradient update and trials that finish without learning. A zero-sized -holdout remains supported when HPO is disabled, including with raw OHLC feature -removal. An interrupted ReforceXY fit is logged and still selects the best -usable checkpoint when available, falling back to the final model otherwise. - -### Reward and portfolio accounting - -The reward logic and tunables are documented in the -[reward space analysis](./ReforceXY/reward_space_analysis/README.md). - -Environment diagnostics `most_recent_return` (log return) and -`most_recent_profit` (simple return) measure changes in liquidation equity, -including unrealized PnL and Freqtrade's staking convention. Actions fill at -`open[t]` while the observation window ends at candle `t-1`; equity marks, PnL -features and trade durations in the returned observation refer to candle `t+1`. -Round-trip fees are provisioned at entry; exits realize at the fill price without -charging fees again. `portfolio_log_returns` stores the same log returns. -Non-positive or non-finite equity produces NaN diagnostics rather than a zero -return. These diagnostics do not change the training reward or realized capital. -Rewards combine the fill-time base components with a potential-based shaping -delta over the returned next observation. Termination liquidates any remaining -position once and clears the terminal potential. `get_env_history()` returns one -metrics/price row per transition. Its `execution_tick` is the transition/action/fill -key before the tick increment; its `tick` is the returned post-increment price and -observation row (normally `execution_tick + 1`). Exit-efficiency extrema include -the fee-adjusted PnL at entry and subsequent retained market marks. Ordered -trade events remain separate in `trade_history`: their tick identifies the -candle whose price filled the event. Action fills use `execution_tick`; -terminal liquidations use the returned post-increment tick. `terminal_liquidation` -and `exit_pnl` remain on the transition history row. - -## Development - -### Runtime regressions - -Run each suite in its matching Freqtrade QA image, with the repository mounted -at `/workspace` and `/workspace` as the working directory: - -```shell -# ReforceXY -python -m unittest discover -s ReforceXY/tests -v - -# QuickAdapter -PYTHONPATH=/workspace/quickadapter/user_data/strategies \ - python -m unittest discover -s quickadapter/tests -v -``` - -QuickAdapter regressions cover normalized FRAMA with an independent numerical -oracle and `custom_exit` with native Freqtrade trades and filled orders. Callback -cases include model expiration, outliers, reversal confirmation and persisted -final take-profit trails for long and short positions, including restart behavior. - -Both commands must be run from the repository root, which is what the -container's `--workdir /workspace` provides. To select one concern, pass a -pattern that matches the whole `module.Class.method` name; a bare substring -selects more than you want, and a pattern that matches nothing runs zero tests -and fails with exit code 5: - -```shell -PYTHONPATH=quickadapter/user_data/strategies \ - python -m unittest discover -s quickadapter/tests -k 'test_utils_zigzag.*' -v -``` - -CI runs type checks and runtime regressions in one QA matrix entry per strategy. -The shared runtime step sets each strategy's `PYTHONPATH`. QuickAdapter needs -this for direct `unittest` discovery because its model imports `LabelTransformer` -and `Utils` by bare names; ReforceXY resolves its imports without it, so the -setting is optional there. QuickAdapter's regressions additionally run under -`coverage.py`; the reward-space analysis suite runs separately with `uv`, -without a Freqtrade image. - -### Coverage gate - -Within the strategy QA matrix, QuickAdapter is the only strategy whose runtime -regressions enforce a coverage gate. The standalone reward-space analysis suite -has its own gate; see [its testing documentation](ReforceXY/reward_space_analysis/tests/README.md). -QuickAdapter's configuration is `quickadapter/.coveragerc`, selected explicitly -because coverage.py looks for `.coveragerc` in the directory it is run from and -does not search parents: - -```shell -export PYTHONPATH=quickadapter/user_data/strategies -export COVERAGE_RCFILE=quickadapter/.coveragerc COVERAGE_FILE=/tmp/.coverage - -sh scripts/run-coverage.sh -s quickadapter/tests -v -``` - -The shared runner gives both coverage commands the same environment. When running -`coverage run` and `coverage report` separately, export both variables for both -commands: prefixing only the run command does not configure the later report, -which then looks in its default data path and fails with `No data to report.` - -Branch coverage is required. Every guard in this codebase is an early return -or a raise, and statement coverage marks a guard covered the instant its `if` -is evaluated. `include_namespace_packages` is required for the opposite -reason: without it coverage skips directories that have no `__init__.py`, so -the `user_data` trees would be absent from the report and the gate would -measure whatever happened to be imported. - -`fail_under` is an absolute floor, never per-module. To change it, re-measure -with the shipped configuration already in place — a measurement taken without -`branch` and `include_namespace_packages` reports a different denominator and -is not a valid input — then update the value and the `# measured` annotation -above it in the same commit. Raise the floor only; a drop needs the reason in -the pull request. `quickadapter/tests/test_coverage_floor.py` refuses a -placeholder, a missing or undated measurement annotation, a measurement below -the floor it justifies, a floor below the current minimum, a floor that is not a -percentage, a `precision` coarse enough to round the total past the floor, a -disabled branch trace, a disabled namespace walk, a source tree that is not the -measured one, a `fail_under` that has drifted into the inert `[run]` section, the -source-level `no cover` or `no branch` pragmas (with or without a colon), -ellipsis-only bodies, additional `TYPE_CHECKING` blocks beyond the existing -import-only block, and any `omit`, `include`, `exclude_lines`, `exclude_also` or -`partial_*` of production code. The shared `scripts/run-coverage.sh` runner is -exercised with real passing and failing coverage reports and a failing test suite; -the checks assert process exit status, not workflow token spelling. - -### Quality checks - -Run repository quality checks from the repository root: - -Ruff does not need the Freqtrade runtime or project dependencies: - -```shell -uvx ruff@latest check . -uvx ruff@latest format --check . -``` - -BasedPyright must run inside the matching Freqtrade QA image. The repository -wrapper records the sorted repository-relative identities of every configured -Python source, requires that inventory to match BasedPyright's analyzed-file -count, and compares it with every emitted diagnostic field—including an optional -rule and source range—against the project's exact snapshot. Build each QA target -and mount the checkout read-only: - -```shell -# QuickAdapter -docker build --pull --target qa --tag freqai-strategies-quickadapter-qa quickadapter -docker run --rm \ - --mount "type=bind,src=$PWD,dst=/workspace,readonly" \ - --entrypoint python \ - freqai-strategies-quickadapter-qa \ - /workspace/scripts/check_basedpyright.py --project quickadapter - -# ReforceXY -docker build --pull --target qa --tag freqai-strategies-reforcexy-qa ReforceXY -docker run --rm \ - --mount "type=bind,src=$PWD,dst=/workspace,readonly" \ - --entrypoint python \ - freqai-strategies-reforcexy-qa \ - /workspace/scripts/check_basedpyright.py --project reforcexy -``` - -The check fails when a configured source identity or diagnostic is added, removed, -moved, or changed, or when the analyzed-file count differs from the source -inventory. Each project's direct `include` entries must be normalized, -non-overlapping relative file or directory paths; glob syntax and symbolic links -are rejected. Snapshot updates are deliberate writable operations in the matching -QA image. For example: - -```shell -docker run --rm \ - --mount "type=bind,src=$PWD,dst=/workspace" \ - --entrypoint python \ - freqai-strategies-quickadapter-qa \ - /workspace/scripts/check_basedpyright.py --project quickadapter --write -``` - -Review the generated `.basedpyright/diagnostics.json` diff. Use the ReforceXY image -and `--project reforcexy` for its snapshot. The writer preserves existing file -permissions and uses mode `0644` when creating a missing snapshot. Snapshot targets -must be regular files; symbolic links and other special files are rejected. The -wrapper rejects direct host and wrong-image execution so Freqtrade imports and -dependency versions remain exact. - -The BasedPyright and type-stub versions are pinned in each project's -`.devcontainer/requirements-dev.txt`, as is `coverage` in QuickAdapter's. -The Freqtrade base images intentionally follow their rolling `stable_freqai` -and `stable_freqairl` tags, so record the resolved image digests when a -reproducible audit is required. +Two Freqtrade strategies with different decision models: + +| Strategy | Decision model | Supplied profile | Operator guide | +| ------------ | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------- | -------------------------------------- | +| QuickAdapter | Regress smoothed Zigzag morphology; enter at calibrated extrema with price confirmation. | Dry-run, spot, 5m; XGBoost; HPO enabled. | [QuickAdapter](quickadapter/README.md) | +| ReforceXY | Reinforcement-learning policy chooses neutral, entry and exit actions. | Dry-run, spot, 5m; MaskablePPO; frame stacking and HPO enabled. | [ReforceXY](ReforceXY/README.md) | + +These profiles are starting configurations, not evidence of profitability. +Training losses and episode rewards are not portfolio performance measures. + +## Documentation + +- [QuickAdapter operation and configuration](quickadapter/README.md): decisions, + prediction warmup, model selection, continuation and all strategy tunables. +- [ReforceXY operation and configuration](ReforceXY/README.md): actions, model and + policy compatibility, masking, HPO, continuation and observation constraints. +- [Evaluation protocol](docs/evaluation.md): chronological comparisons, costs, + economic outcomes, uncertainty and limits of native backtesting. +- [Reward-space analysis](ReforceXY/reward_space_analysis/README.md): synthetic + reward diagnostics and trusted real-data imports; not strategy profitability. +- [Contribution guide](CONTRIBUTING.md): matching runtime images, coverage, lint, + types and [analysis-suite maintenance](ReforceXY/reward_space_analysis/tests/README.md). + +## Start safely + +Follow the relevant guide's quick start. Both supplied configurations enable +`dry_run` and disable the API server. Review exchange, pair universe, sizing, +protections and costs before changing trading mode or disabling dry-run. Private +exchange credentials are needed only for operations that require them; never +commit secrets or the working `user_data/config.json`. + +Before enabling the API, replace its public template username, password, JWT +secret and WebSocket token. Compose binds the exposed API port to localhost; +retain that restriction unless access is protected by a VPN or SSH tunnel. +Review the Compose timezone for your location. + +The builds follow moving Freqtrade `stable_freqai` / `stable_freqairl` tags and +resolve dependencies at build time. Record resolved image digests and dependency +versions for reproducible evaluations. ## Common workflows -**List running compose services and the containers they created:** +Run Compose commands from the selected strategy directory, using the same +Compose project name and environment as when the stack was started. ```shell docker compose ps -``` - -**Enter a running service:** - -```shell -# use the compose service name (e.g. "freqtrade") docker compose exec freqtrade /bin/sh +docker compose logs -f freqtrade ``` -**View logs:** +`freqtrade` is the service name in both supplied stacks. The container names are +`freqtrade-quickadapter` and `freqtrade-ReforceXY`; for example: ```shell -# service logs (compose maps service -> container(s)) -docker compose logs -f freqtrade - -# or follow a specific container's logs docker logs -f freqtrade-quickadapter ``` -**Stop and remove the compose stack:** +`docker compose down` stops the bot and removes its containers. It does not +close positions or necessarily cancel exchange orders. Plan maintenance around +open exposure and back up configuration, database, models and HPO state. -```shell -docker compose down -``` +## Image updates -**Automatically update docker images:** +The updater must be copied into the selected strategy directory: it locates +Compose and the default configuration relative to itself. Its built-in image +values are ReforceXY-specific; QuickAdapter requires a different remote base. + +### QuickAdapter + +From the repository root: ```shell -cd ReforceXY # or quickadapter +cd quickadapter cp ../scripts/docker-upgrade.sh . +LOCAL_DOCKER_IMAGE="$(docker compose config --images)" \ +REMOTE_DOCKER_IMAGE=freqtradeorg/freqtrade:stable_freqai \ ./docker-upgrade.sh ``` -The script checks for new Freqtrade image versions on Docker Hub, rebuilds and -restarts containers if updates are found, sends Telegram notifications (if -configured), and cleans up unused images. +### ReforceXY -_Configuration and environment variables:_ +From the repository root: -| Variable | Default | Description | -| ------------------- | ---------------------------------------- | ------------------------------------ | -| FREQTRADE_CONFIG | `./user_data/config.json` | Freqtrade configuration file path | -| LOCAL_DOCKER_IMAGE | `reforcexy-freqtrade` | Local image name | -| REMOTE_DOCKER_IMAGE | `freqtradeorg/freqtrade:stable_freqairl` | Freqtrade image to track for updates | +```shell +cd ReforceXY +cp ../scripts/docker-upgrade.sh . +LOCAL_DOCKER_IMAGE="$(docker compose config --images)" \ +REMOTE_DOCKER_IMAGE=freqtradeorg/freqtrade:stable_freqairl \ +./docker-upgrade.sh +``` -_Cronjob setup (daily check at 3:00 AM):_ +The shipped stacks each resolve one local image: `quickadapter-freqtrade` and +`reforcexy-freqtrade` with their default project names. Deriving it from Compose +also handles `COMPOSE_PROJECT_NAME` overrides. With a customized multi-image +stack, inspect `docker compose config --images` and set only the `freqtrade` +service's image. Keep the same project/environment for the updater. + +| Variable | Built-in fallback | Meaning | +| --------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------- | +| `FREQTRADE_CONFIG` | `