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.
-
# 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"
# - "./subproject1"
# - "./subproject2"
ls_workspace_folders:
-- .
+ - .
# list of language servers to start when using the LSP backend; choose from:
# ada al angular ansible bash
# 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).
--- /dev/null
+# 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.
# 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 * <fill_epsilon_baseline>(pivot_weights)`; `gaussian` applies per-pivot Gaussian bumps; `epsilon_gaussian` sums the `epsilon` floor and the `gaussian` bumps. Pivot rows take the max of their raw weight and the off-pivot field at their index (no-op for `zero`). 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<label_horizon_candles`; (3) for `train_test_split`, applies the same fixed `label_horizon_candles` purge around the train/test boundary; (4) both split methods additionally drop any train row whose label-aware availability reaches the test boundary; (5) label weighting becomes causal: the `epsilon` baseline at each row uses only pivot weights available with that row's label. `false` is deprecated: the causal split-guard rejections are lifted, but the toggle-independent ones remain (`timeseries_split` still rejects `shuffle_after_split`, and an active holdout still rejects all three at evaluation); `timeseries_split` `gap` auto-sets from `label_period_candles`, and Gaussian fills keep unbounded tails. |
-| freqai.feature_parameters.min_label_period_candles | 12 | int >= 1 | Minimum labeling NATR period used for reversals labeling HPO. |
-| freqai.feature_parameters.max_label_period_candles | 24 | int >= 1 | Maximum labeling NATR period used for reversals labeling HPO. |
-| freqai.feature_parameters.label_natr_multiplier | min/max midpoint | float > 0 | Zigzag labeling NATR multiplier. |
-| freqai.feature_parameters.min_label_natr_multiplier | 9.0 | float > 0 | Minimum labeling NATR multiplier used for reversals labeling HPO. |
-| freqai.feature_parameters.max_label_natr_multiplier | 12.0 | float > 0 | Maximum labeling NATR multiplier used for reversals labeling HPO. |
-| freqai.feature_parameters.label_frequency_candles | `auto` | int [2, 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.<glob>`. 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` | `<script directory>/user_data/config.json` | Configuration used for optional Telegram notifications; override for a different path. |
+| `LOCAL_DOCKER_IMAGE` | `reforcexy-freqtrade` | Local Compose image to archive and remove before recreation. |
+| `REMOTE_DOCKER_IMAGE` | `freqtradeorg/freqtrade:stable_freqairl` | Base-image tag whose image ID is checked for updates. |
+
+When that remote tag changes, the script stops the stack, archives the previous
+local image, attempts to remove its current tag, recreates the stack and prunes
+unused images. This introduces downtime. Read warnings: a failed image removal
+can leave the old image available to Compose. The script does not detect source
+or dependency updates when the remote image ID is unchanged. For an explicit
+rebuild, use `docker compose build --pull`, then `docker compose up -d`.
+
+The latest successful archive tag is logged as
+`<local image repository>-archive:<previous image ID without sha256:>`; older
+archives are removed. Record it before relying on rollback. Stop the affected
+stack, retag that archived image to its resolved local image name, then use
+`docker compose up -d --no-build`. An image rollback does not undo trades,
+configuration changes or database/model migrations; retain compatible state
+backups and inspect exposure before restarting.
+
+For example, a daily QuickAdapter check at 03:00 (after creating
+`user_data/logs` and adapting the absolute checkout path):
```cron
-0 3 * * * cd /path/to/freqai-strategies/ReforceXY && ./docker-upgrade.sh >> user_data/logs/docker-upgrade.log 2>&1
+0 3 * * * cd /path/to/freqai-strategies/quickadapter && LOCAL_DOCKER_IMAGE="$(docker compose config --images)" REMOTE_DOCKER_IMAGE=freqtradeorg/freqtrade:stable_freqai ./docker-upgrade.sh >> user_data/logs/docker-upgrade.log 2>&1
```
----
+For ReforceXY, use its directory and `stable_freqairl`. Schedule automated
+restarts only when their downtime and open-position risk are acceptable.
## Note
--- /dev/null
+# ReforceXY
+
+ReforceXY trains a reinforcement-learning policy. RLAgentStrategy translates its
+five actions into Freqtrade entry/exit signals; episode reward is not portfolio
+profitability.
+
+## Contents
+
+- [Quick start](#quick-start)
+- [Actions and safety](#actions-and-safety)
+- [Supplied profile and effective configuration](#supplied-profile-and-effective-configuration)
+- [Model and policy compatibility](#model-and-policy-compatibility)
+- [Training and HPO](#training-and-hpo)
+- [Continual learning](#continual-learning)
+- [Live inference and backtesting](#live-inference-and-backtesting)
+- [Configuration reference](#configuration-reference)
+- [Reward and portfolio accounting](#reward-and-portfolio-accounting)
+
+## Quick start
+
+From the repository root:
+
+```shell
+cd ReforceXY
+cp user_data/config-template.json user_data/config.json
+```
+
+Review exchange, pairs, sizing and `freqai` in `user_data/config.json`. Keep
+dry-run enabled while checking behavior. Add private exchange credentials only
+for operations that require them. Review the timezone in
+[docker-compose.yml](docker-compose.yml) and the
+[API/security and maintenance guidance](../README.md#start-safely).
+
+The supplied profile enables HPO, eight training environments and state
+observations. For a smaller first run, disable `freqai.rl_config_optuna.enabled`,
+use `n_envs=1` and `multiprocessing=false`; these are intentional overrides,
+not the shipped profile. For native backtests, also follow the state-observation
+restrictions below.
+
+```shell
+docker compose up -d --build
+```
+
+The build follows the moving `stable_freqairl` base. Record resolved digests and
+dependency versions for reproducible evaluations.
+
+## Actions and safety
+
+| Predicted action | Value | Strategy decision when `do_predict=1` |
+| ---------------- | ----- | -------------------------------------------------------- |
+| Neutral | 0 | No new entry or ordinary exit signal. |
+| Long_enter | 1 | Long entry. |
+| Long_exit | 2 | Long exit. |
+| Short_enter | 3 | Short entry; shorting is allowed only in margin/futures. |
+| Short_exit | 4 | Short exit. |
+
+`do_predict=0` does not generate these ordinary signals. On the final candle,
+`do_predict=2` (expired model) requests exits for the pair's open positions,
+independently of the predicted action. Freqtrade capacity, protections, order
+handling and exchange execution still determine whether an order is placed or
+filled.
+
+Only MaskablePPO supports action masking in this integration.
+`inference_masking=false` disables its prediction-time mask, not its training
+or evaluation masks. Live masks use the real position even without state
+observations. Do not set `rl_config.action_masking` independently: it cannot
+add masking support to another algorithm and can make reward invalid-action
+handling inconsistent with the selected model.
+
+Reward `freqai.rl_config.model_reward_parameters.max_trade_duration_candles`
+and `freqai.rl_config.model_reward_parameters.max_idle_duration_candles`
+normalize duration-dependent reward components; they are not live exit timers.
+`max_training_drawdown_pct` limits training episodes, not exchange losses.
+`RLAgentStrategy` does not turn these training settings into a live stoploss.
+Review strategy protections, position size and execution risk separately.
+The supplied exchange configuration sets `stoploss=-0.99`; the training
+drawdown value 0.02 is not a replacement for reviewing that live risk limit.
+
+## Supplied profile and effective configuration
+
+[config-template.json](user_data/config-template.json) supplies dry-run, spot,
+5m, MaskablePPO/MlpPolicy, 25 training cycles, `n_envs=8`, multiprocessing,
+`frame_stacking=2`, `add_state_info=true`, holdout 0.333 and HPO enabled for
+100 trials with no timeout. Runtime fallbacks differ: one environment, no
+multiprocessing or stacking, HPO disabled and optional action statistics disabled.
+
+[RLAgentStrategy](user_data/strategies/RLAgentStrategy.py) adds log returns,
+volume, calendar and raw OHLC features, but does not implement a period-dependent
+`feature_engineering_expand_all()` callback. The supplied
+`freqai.feature_parameters.indicator_periods_candles=[8,16,32]` therefore adds
+no period-dependent indicators.
+
+Explicit configuration overrides schema defaults and constructor fallbacks. The
+tables distinguish those defaults and requirements from the supplied profile;
+inspect normalized startup logs. Set `test_size`
+explicitly: the local HPO enablement check falls back to 0.1 when omitted, but
+the native split is 25% when both `test_size` and `train_size` are omitted.
+Do not treat the HPO gate fallback as an effective data-split default.
+
+## Model and policy compatibility
+
+These five algorithms are supported by the application's HPO path and decision
+workflow. Policy names are not automatically changed when `model_type` changes.
+
+| Model | Policy for the numerical observations used here | Masking |
+| ------------ | ----------------------------------------------- | ------------------------------------------------ |
+| PPO | MlpPolicy | No |
+| MaskablePPO | MlpPolicy | Training/evaluation; inference toggle available. |
+| RecurrentPPO | MlpLstmPolicy | No |
+| DQN | MlpPolicy | No |
+| QRDQN | MlpPolicy | No |
+
+Changing only the supplied model name to RecurrentPPO leaves MlpPolicy in place
+and fails construction. Use MlpLstmPolicy and prefer `frame_stacking=0`:
+recurrent memory plus stacked frames is permitted but warns about redundancy.
+For LSTM policy kwargs, `shared_lstm=true` requires
+`enable_critic_lstm=false`; plain fits forward these settings to the SDK.
+Use a new `freqai.identifier` for a different policy/observation architecture.
+
+A backtest-safe **override fragment**, to merge into a complete Freqtrade
+configuration (not a standalone exchange config):
+
+```json
+{
+ "leverage": 1,
+ "freqai": {
+ "enabled": true,
+ "identifier": "ReforceXY-RecurrentPPO",
+ "continual_learning": false,
+ "data_split_parameters": {
+ "test_size": 0.333,
+ "shuffle": false
+ },
+ "model_training_parameters": {
+ "device": "cpu",
+ "policy_kwargs": {
+ "net_arch": "small",
+ "shared_lstm": false,
+ "enable_critic_lstm": true
+ }
+ },
+ "rl_config": {
+ "model_type": "RecurrentPPO",
+ "policy_type": "MlpLstmPolicy",
+ "train_cycles": 25,
+ "n_envs": 1,
+ "n_eval_envs": 1,
+ "multiprocessing": false,
+ "eval_multiprocessing": false,
+ "frame_stacking": 0,
+ "add_state_info": false,
+ "model_reward_parameters": {
+ "rr": 2,
+ "profit_aim": 0.025,
+ "hold_potential_enabled": false
+ },
+ "check_envs": true,
+ "plot_new_best": false
+ },
+ "rl_config_optuna": {
+ "enabled": false
+ }
+ }
+}
+```
+
+## Training and HPO
+
+HPO runs only when `freqai.enabled=true`,
+`freqai.rl_config_optuna.enabled=true` and the effective configured
+`freqai.data_split_parameters.test_size > 0`. Disable it explicitly with
+`enabled=false` in `rl_config_optuna`. A zero holdout disables evaluation/HPO
+and remains supported for plain training, including with raw OHLC feature
+removal.
+
+`train_cycles` requests approximately training rows × cycles per HPO trial
+and final fit; environment and algorithm update boundaries round the actual
+budget. It is not an exact episode count. Trials are serialized (one Optuna
+worker), although each learner can use several environments. Bound `n_trials`
+and use a positive `timeout_hours` for a soft HPO wall-clock budget checked
+between trials. Optuna does not interrupt a running trial when that budget
+expires, so the current trial can exceed it. The subsequent final fit and
+evaluation are outside this timeout. The supplied zero timeout disables the HPO
+timeout. Evaluation environments/episodes also cost resources; a larger budget
+is not evidence of better economics.
+
+The objective maximizes the best mean deterministic evaluation **episode reward**
+across periodic and final evaluations. Progress may display the latest mean
+reward, which is not the selected best mean reward. The objective is not net
+portfolio profit, Sharpe or Sortino. The holdout participates in model/HPO
+selection; reserve a separate unseen market period for economic confirmation
+under the [evaluation protocol](../docs/evaluation.md).
+
+Optuna trains fresh candidates in current-window feature coordinates. Sampled
+parameters override supplied model parameters during trials. Hyperband prunes
+unpromising trials; invalid rollout/batch/buffer combinations and DQN-family
+warmup budgets without gradient updates are rejected. An explicitly sampled
+`target_kl=null` disables KL stopping even if a numeric value was configured.
+There is no user-configurable objective or search-range schema in this wrapper.
+
+With continual learning, the final fit resumes the deployed policy in frozen
+feature coordinates, including its discount factor; otherwise it trains a fresh
+policy. Selected constructor parameters do not rebuild a resumed architecture.
+The current evaluation's best usable checkpoint is deployed; absent one, the
+final policy is used. Interrupted fits are logged and follow the same checkpoint
+selection. Compatible prior best parameters can be reused after an unsuccessful
+search; absent those, effective plain model parameters remain the fallback.
+
+## Continual learning
+
+Continuation requires `freqai.continual_learning=true` and effective
+`freqai.rl_config.frame_stacking=0`. Nonzero stacking disables continuation
+with a warning even if it was requested. The supplied stacking value is 2;
+change it explicitly before enabling continuation. Restoring a saved policy for
+inference is not the same as continuing its training.
+
+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 and backtesting
+
+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.
+Optional action statistics do not gate RL actions.
+
+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.
+
+For native backtests, set both `freqai.rl_config.add_state_info=false` and
+`freqai.rl_config.model_reward_parameters.hold_potential_enabled=false`.
+Use a fresh identifier after observation-shape changes. Keep chronological
+splits; training disables `shuffle_after_split`, and the native RL base
+disables row-removing SVM/DBSCAN/DI outlier removal to preserve trajectories.
+
+## Configuration reference
+
+The implementation and runtime validation are in
+[ReforceXY.py](user_data/freqaimodels/ReforceXY.py); the supplied profile is
+[config-template.json](user_data/config-template.json). Expected types below
+are not promises of validation where the table says an option is forwarded or
+not locally validated. This guide covers ReforceXY behavior, overrides and SDK
+boundaries. For standard framework settings, use the
+[Freqtrade configuration reference](https://www.freqtrade.io/en/stable/configuration/)
+and the [FreqAI parameter reference](https://www.freqtrade.io/en/stable/freqai-parameter-table/).
+Reward formulas and tunable descriptions are in the [reward reference](reward_space_analysis/README.md#reward-tunables-reference); live environment defaults are defined by `ReforceXY.DEFAULT_*` and the native RL base.
+
+Normal Freqtrade startup injects declared JSON-schema defaults before the FreqAI
+model is initialized. In the runtime table, **Schema** values apply to omitted
+keys after validation; **direct-constructor** values describe initialization
+without that injection. Other fallback entries apply when a native or app field
+remains omitted. Supplied values take precedence over defaults. These schema
+values were verified with Freqtrade 2026.8 and 2026.9; see the
+[versioned native schema](https://github.com/freqtrade/freqtrade/blob/2026.8/freqtrade/config_schema/config_schema.py).
+Moving base images can change them; record the runtime version.
+
+- [Runtime, observations and evaluation](#runtime-observations-and-evaluation)
+- [HPO settings](#hpo-settings)
+- [Model constructor and policy parameters](#model-constructor-and-policy-parameters)
+
+### Runtime, observations and evaluation
+
+| Path | Schema default / constructor fallback / requirement | Type / constraints | Supplied profile | Behavior |
+| ------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| freqai.rl_config.model_type | Schema: PPO; direct constructor requires the key | String: app-supported PPO, RecurrentPPO, MaskablePPO, DQN, QRDQN | MaskablePPO | Omitting the key selects PPO, not the supplied MaskablePPO profile. Native loader also accepts A2C/TRPO/ARS, but ReforceXY HPO explicitly does not support them; do not present them as supported app HPO choices. |
+| freqai.rl_config.policy_type | Schema: MlpPolicy; direct constructor requires the key | Algorithm-compatible SB3 policy name | MlpPolicy | Use MlpLstmPolicy with RecurrentPPO; MlpPolicy with ordinary PPO/MaskablePPO/DQN/QRDQN. The schema does not adapt the default to model_type. Forwarded to model constructor, not renamed by app. |
+| freqai.continual_learning | false | Boolean | Omitted | Requires frame_stacking=0. Nonzero effective stacking disables continuation. Keeps deployed fitted feature coordinates/pipeline and policy; architecture changes require new identifier/reset; HPO remains cold/current-window. |
+| freqai.rl_config.train_cycles | 25 | Integer intended; int(value), clamped to minimum 1; no app upper bound | 25 | Requested budget is training rows × cycles, rounded to environment/model update boundaries; shared by each HPO trial and final fit. Not an exact number of completed episodes. |
+| freqai.data_split_parameters.test_size | Gate fallback 0.1; actual omitted split 25% if train_size also omitted | 0 disables evaluation/HPO; otherwise sklearn-valid holdout fraction/count with nonempty training and test datasets | 0.333 | Set explicitly. HPO additionally requires both freqai.enabled and rl_config_optuna.enabled. Native split forced chronological. |
+| freqai.rl_config.n_envs | 1 | Integer >=1; invalid reset to 1 | 8 | Parallel training environments; multiplication affects rollout budget and resource requirements. |
+| freqai.rl_config.n_eval_envs | 1 | Integer >=1; invalid reset to 1 | Omitted | Evaluation environments exist only when test_size>0. |
+| freqai.rl_config.multiprocessing | false | Boolean | true | SubprocVecEnv only when n_envs>1; otherwise forced false. Disables plot_new_best. |
+| freqai.rl_config.eval_multiprocessing | false | Boolean | Omitted | SubprocVecEnv only when n_eval_envs>1; otherwise forced false. |
+| freqai.rl_config.frame_stacking | 0 | Integer >=0; invalid/1 normalize to 0; >1 enables stacks | 2 | Nonzero effective stacking disables continual learning. RecurrentPPO warns redundancy but does not forcibly remove stacks. |
+| freqai.rl_config.action_masking | Derived from model_type == MaskablePPO when omitted | Boolean; do not override independently | Omitted | Model-side training/evaluation/inference compatibility is derived from exact MaskablePPO model name; an explicit field only changes environment invalid-action reward handling, not model capability. Keep omitted to avoid inconsistent settings. |
+| freqai.rl_config.inference_masking | true | Boolean | Omitted | Only effective for MaskablePPO; RecurrentPPO does not support maskable inference. Disabling does not disable training/evaluation masking. |
+| freqai.rl_config.lr_schedule | false | Boolean | false | Plain fit only: turns numeric learning_rate into clamped linear initial-to-zero schedule. HPO samples its own schedule. |
+| freqai.rl_config.cr_schedule | false | Boolean | false | Plain fit only, PPO family only: turns numeric clip_range into clamped linear initial-to-zero schedule. HPO samples its own schedule. |
+| freqai.rl_config.n_eval_steps | 10000 | Integer >0; invalid reset to 10000 | Omitted | Non-PPO evaluation interval in aggregate timesteps, converted to callback calls via ceil(n_eval_steps/n_envs). PPO prefers n_steps/rollout-sized interval instead. HPO reduces interval by factor 4. |
+| freqai.rl_config.n_eval_episodes | 5 | Integer >0; invalid reset to 5 | Omitted | Episodes averaged in periodic and final deterministic policy evaluation. |
+| freqai.rl_config.max_no_improvement_evals | 0 | Integer intended; 0 disables; no local range/type validation | 0 | Adds SB3 StopTrainingOnNoModelImprovement for ordinary fit evaluation, not HPO trial callbacks. |
+| freqai.rl_config.min_evals | 0 | Integer intended; no local range/type validation | 0 | Wait evaluations before no-improvement stopping is counted; relevant only with max_no_improvement_evals enabled. |
+| freqai.rl_config.check_envs | true | Boolean | true | Gym API smoke validation of training env and, with test_size>0, eval env. |
+| freqai.rl_config.tensorboard_throttle | 1 | Integer >=1; invalid reset to 1 | Omitted | When activate_tensorboard is enabled, InfoMetricsCallback collects metrics when aggregate num_timesteps is divisible by this value (1 collects on every callback call). This is a timestep-modulo filter, not a callback-call interval: with n_envs=8 and throttle=8, every call passes. TensorBoard write/flush frequency is separate. |
+| freqai.rl_config.plot_new_best | false | Boolean | false | TensorBoard rollout plot on a new best ordinary-fit checkpoint; disabled with training multiprocessing. |
+| freqai.rl_config.plot_window | 2000 | Integer intended; positive truncates; <=0 retains full history; no local type validation | Omitted | Environment history rows retained in rollout plot. |
+| freqai.rl_config.progress_bar | Schema: true; direct-constructor fallback: false | Boolean | Omitted | Enables training progress callback and Optuna progress display; omitted validated configuration enables it. |
+| freqai.rl_config.add_state_info | false | Boolean | true | Adds unlevered unrealized PnL, position, trade duration to observations; unsupported in backtesting. hold_potential_enabled automatically enables it, so hold potential also cannot be backtested. |
+| freqai.rl_config.cpu_count | 1 | Positive integer suitable for torch threads; no app validation | 4 | Native torch thread count capped by half max_system_threads; separate from environment count. |
+| freqai.rl_config.max_training_drawdown_pct | Schema: 0.02; direct-constructor fallback: 0.8 | Numeric intended; no local range validation; do not claim enforced [0,1] | 0.02 | Native equity floor is 1-value, so an omitted validated value gives 0.98. Training episode risk cutoff, not a live exchange stoploss. |
+| freqai.fit_live_predictions_candles | 0 | Nonnegative integer; bool rejected; invalid raises ValueError | Omitted | 0 disables action mean/population std statistics; live uses persisted produced observations, backtest prior rows; statistics do not gate RL actions. |
+| freqai.rl_config.model_reward_parameters | Required map; no schema or application fallback | Dictionary; rr and profit_aim are schema-defaulted members | rr=2, profit_aim=0.025, max_trade_duration_candles=96, idle_penalty_ratio=0 | Include the map. Schema validation supplies omitted rr/profit_aim members; a direct native constructor requires both. Analyzer defaults do not supply this map. Optional reward settings have their own environment fallbacks. Reward shaping and duration normalization are not live order timeouts. Model gamma overrides potential_gamma when environments are constructed. |
+| freqai.rl_config.model_reward_parameters.rr | Schema: 1; direct constructor requires the member | Numeric reward-to-risk factor | 2 | The schema injects an omitted member into an existing reward map; direct environment construction without it raises. Native reward target uses rr × profit_aim. |
+| freqai.rl_config.model_reward_parameters.profit_aim | Schema: 0.025; direct constructor requires the member | Numeric profit target | 0.025 | The schema injects an omitted member into an existing reward map; direct environment construction without it raises. This reward target does not place a live take-profit order. |
+| freqai.rl_config.model_reward_parameters.max_trade_duration_candles | 128 | Integer-convertible value; no local range validation | 96 | Duration scale for reward penalties, not a live timeout. The schema accepts the top-level homonym `freqai.rl_config.max_trade_duration_candles`, but ReforceXY ignores it; only this reward-map member takes effect. |
+| freqai.rl_config.model_reward_parameters.max_idle_duration_candles | 4 × effective max_trade_duration_candles | Integer-convertible value; no local range validation | Omitted (384 from the supplied trade scale) | Idle duration scale for reward penalties, not a live timer. |
+
+### HPO settings
+
+| Path | Runtime fallback / requirement | Type / constraints | Supplied profile | Behavior |
+| ---------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| freqai.rl_config_optuna.enabled | false | Boolean | true | Requires freqai.enabled=true and test_size>0; optimize PPO/RecurrentPPO/MaskablePPO/DQN/QRDQN. |
+| freqai.rl_config_optuna.n_trials | 100 | Integer intended; forwarded to Optuna, no local range validation | 100 | New trial budget per optimize call; does not mean lifetime cap on reusable study; serialized n_jobs=1. |
+| freqai.rl_config_optuna.n_startup_trials | 15 | Integer intended; forwarded to TPE, no local range validation | 15 | TPE random-startup trials; not passed to AutoSampler. |
+| freqai.rl_config_optuna.timeout_hours | 0 | Numeric hours; no local range validation | 0 | 0 disables the HPO timeout; nonzero values are multiplied by 3600 and passed to study.optimize. Positive values set a soft budget checked between trials: a running trial may exceed it, and the subsequent final fit/evaluation are outside it. Does not replace train_cycles. |
+| freqai.rl_config_optuna.continuous | false | Boolean | Omitted | IMPORTANT: true deletes/recreates the study each optimize call, not endless trial execution. false reuses compatible objective-identity study. |
+| freqai.rl_config_optuna.warm_start | false | Boolean | Omitted | Enqueues saved compatible best parameters. A purge-triggered run enqueues previous best even if warm_start=false. |
+| freqai.rl_config_optuna.sampler | tpe | tpe \| auto; invalid raises ValueError | Omitted | tpe uses multivariate/group TPE; auto loads OptunaHub auto sampler and its dependencies. |
+| freqai.rl_config_optuna.storage | sqlite | sqlite \| file; invalid raises ValueError | Omitted | sqlite RDB or journal file under identifier model directory; filename uses base symbol. |
+| freqai.rl_config_optuna.purge_period | 0 | int(value) conversion occurs before validator; negative reset to 0; unconvertible raises during initialization | Omitted | 0 disables periodic purge; positive resets every X pair retrains; ignored/forced 0 when continuous=true. |
+| freqai.rl_config_optuna.seed | 42 | Integer intended; no local validation | Omitted | Sampler seed; distinct from model_training_parameters.seed used for models/envs. |
+
+### Model constructor and policy parameters
+
+| Path | Runtime fallback / requirement | Type / constraints | Supplied profile | Behavior |
+| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| freqai.model_training_parameters | {} native | Dictionary | device:auto, verbose:1 | Deep-copied then normalized; remaining kwargs forwarded to chosen SB3 constructor. This is SDK boundary, not an app-owned exhaustive hyperparameter schema. |
+| freqai.model_training_parameters.seed | 42 | Integer intended; SDK validation | Omitted | Local seed default; envs derive seeds and HPO increments by trial number. |
+| freqai.model_training_parameters.gamma | 0.95 | Numeric SDK discount; no local app range validation | Omitted | Local default; propagated to environment potential_gamma. Resumed learner keeps its own gamma. |
+| freqai.model_training_parameters.learning_rate | SDK default unless lr_schedule=true; then initial 0.0003 if omitted | SDK numeric/schedule; app wraps numeric only | Omitted | Do not call 0.0003 an unconditional app default. |
+| freqai.model_training_parameters.clip_range | SDK default unless PPO-family cr_schedule=true; then initial 0.2 if omitted | SDK numeric/schedule; app wraps numeric only | Omitted | Only PPO family schedule conversion. |
+| freqai.model_training_parameters.gpu_memory_fraction | null/omitted disables | Numeric in (0,1] intended; ignored invalid bounds/no CUDA, not robust type validation | Omitted | App-only; applies torch CUDA per-process limit to device 0, removed before SDK constructor. |
+| freqai.model_training_parameters.device | No app fallback; SDK auto | PyTorch/SB3 device specification | auto | Forwarded; doc examples auto/cpu/cuda/cuda:0 are not an app-enforced exhaustive choice list. |
+| freqai.model_training_parameters.verbose | SDK constructor default; callback lookup fallback 0 | SDK verbosity integer | 1 | Forwarded to constructor, also read for callback verbosity. |
+| freqai.model_training_parameters.policy_kwargs.net_arch | [128,128] per branch for PPO family; [128,128] for others | small\|medium\|large\|extra_large, list of layer widths; PPO family additionally {pi:[...],vf:[...]} | Omitted | Presets use two layers of 128/256/512/1024; PPO list expanded to both heads, invalid/missing head resets to [128,128]. Other families accept list or presets. Layer element validation remains SDK. |
+| freqai.model_training_parameters.policy_kwargs.activation_fn | relu | relu\|tanh\|elu\|leaky_relu | Omitted | Converted to torch class; unknown name silently falls back to ReLU. |
+| freqai.model_training_parameters.policy_kwargs.optimizer_class | adamw | adamw\|rmsprop\|adam | Omitted | Converted to torch class; unknown name falls back to Adam (not AdamW). |
+| freqai.model_training_parameters.subsample_steps | Absent | Positive integer intended; no local rejection | Omitted | DQN/QRDQN-only app convenience, removed before constructor; computes gradient_steps=min(train_freq,max(1,ceil(train_freq/subsample_steps))) when both positive integers. Otherwise -1. |
+| freqai.model_training_parameters.gradient_steps | DQN-family absent/None -> derived value, usually -1 without valid train_freq+subsample_steps | SDK integer | Omitted | Explicit non-None value retained; not an unconditional SDK-default forwarding. |
+| leverage | proposed_leverage, not fixed 1 | Finite float-convertible requested value; effective clamped [1,max_leverage] | Omitted (the commented example is not an override) | Top-level strategy field, not an RL/model parameter. Invalid/nonfinite values use the proposal; live state PnL is divided by trade leverage to align the training proxy. |
+| custom_protections | [] | List of dictionaries, each with a string method; otherwise ValueError | [] | Top-level strategy field. Protection-specific settings are delegated to Freqtrade protection plugins. An empty list does not configure custom protections. |
+
+Unlisted algorithm-specific constructor kwargs are forwarded to the selected
+SDK after application normalization; use the compatible algorithm's documented
+parameters, not a common parameter list for all models:
+[PPO](https://stable-baselines3.readthedocs.io/en/master/modules/ppo.html),
+[DQN](https://stable-baselines3.readthedocs.io/en/master/modules/dqn.html),
+[MaskablePPO](https://sb3-contrib.readthedocs.io/en/master/modules/ppo_mask.html),
+[RecurrentPPO](https://sb3-contrib.readthedocs.io/en/master/modules/ppo_recurrent.html),
+[QRDQN](https://sb3-contrib.readthedocs.io/en/master/modules/qrdqn.html).
+Application HPO uses its fixed algorithm-specific search, not every SDK option.
+
+LSTM `shared_lstm`, `enable_critic_lstm`, `lstm_hidden_size` and `n_lstm_layers`
+are SDK policy kwargs. HPO forces the critic LSTM off when a shared LSTM is
+selected; plain fits forward the configured combination. Use
+`model_training_parameters.policy_kwargs.net_arch` for this model:
+`rl_config.net_arch` is not an equivalent architecture override.
+
+## Reward and portfolio accounting
+
+The reward logic and tunables are documented in the
+[reward space analysis](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.
- [2. Parameter Sensitivity](#2-parameter-sensitivity)
- [3. Debug Anomalies](#3-debug-anomalies)
- [4. Real vs Synthetic](#4-real-vs-synthetic)
+- [Trusted pickle inputs](#trusted-pickle-inputs)
- [CLI Parameters](#cli-parameters)
+ - [Boolean syntax](#boolean-syntax)
- [Simulation & Environment](#simulation--environment)
- [Hybrid Simulation Scalars](#hybrid-simulation-scalars)
- [Reward & Shaping](#reward--shaping)
uv run python reward_space_analysis.py --num_samples 20000 --out_dir reward_space_outputs
```
-See `statistical_analysis.md` (1–3): positive exit averages (long & short),
-negative invalid penalties, monotonic idle reduction, zero invariance failures.
+Use the report's component decomposition, state/action legality and finite-value
+checks, not a required positive exit mean. Losing exits can have negative reward.
+The default spot+masking run contains no short positions and no invalid-action
+penalties, so those branches are not evaluated by that command.
+
+Exercise distinct regimes explicitly (small diagnostic runs, not economic evidence):
+
+```shell
+# Valid short actions as well as longs
+uv run python reward_space_analysis.py --num_samples 2000 --seed 42 \
+ --trading_mode futures --skip_feature_analysis --out_dir validation_futures
+# Invalid-action penalties; attempted shorts in spot must not create short positions
+uv run python reward_space_analysis.py --num_samples 2000 --seed 42 \
+ --params action_masking=false --skip_feature_analysis --out_dir validation_unmasked
+# Active canonical shaping with no additive terms
+uv run python reward_space_analysis.py --num_samples 2000 --seed 42 \
+ --exit_potential_mode canonical --hold_potential_enabled 1 \
+ --entry_additive_enabled 0 --exit_additive_enabled 0 \
+ --skip_feature_analysis --out_dir validation_canonical
+```
+
+Inspect actual action counts before judging an unexercised branch. The invalid
+component follows the configured penalty; the complete shaped reward need not
+have the same sign. Idle-penalty monotonicity applies to its component under
+fixed parameters, not arbitrary total trajectory rewards. PBRS verification
+requires complete ordered trajectories, local shaping identities, adjacent
+potential continuity and the discounted terminal boundary. A verified observed
+trajectory is not a proof of universal policy invariance, and noncanonical modes
+must not be certified canonical merely because correction is zero.
### 2. Parameter Sensitivity
### 4. Real vs Synthetic
+Read [Trusted pickle inputs](#trusted-pickle-inputs) before importing an artifact.
+
```shell
uv run python reward_space_analysis.py \
--num_samples 100000 \
Generates shift metrics for comparison (see Outputs section).
+## Trusted pickle inputs
+
+Only load pickle files produced by a source you trust. Deserialization can
+execute arbitrary code **before** column validation. Neither schema checks nor
+the recorded SHA-256 makes an untrusted pickle safe.
+
+The CLI requires these numeric columns: `pnl`, `trade_duration`,
+`idle_duration`, `position`, `action` and `reward`.
+Positions use Short=0, Long=1, Neutral=0.5; actions use Neutral=0,
+Long_enter=1, Long_exit=2, Short_enter=3, Short_exit=4. Store numeric values,
+not enum objects. Missing required columns fail the CLI. Nonnumeric values and
+infinities become NaN with warnings; this is not full semantic validation of a
+trading trajectory.
+
+Accepted envelopes:
+
+- A pandas DataFrame or other DataFrame-compatible records/column arrays.
+- A dictionary with `transitions` containing a DataFrame or iterable of records.
+- A list of episode dictionaries with such `transitions` members. Entries
+ without `transitions` are skipped with a warning.
+
+A complete, locally controlled format example (not empirical trading evidence):
+
+```shell
+uv run python - <<'PY'
+from pathlib import Path
+import pickle
+
+rows = [{"pnl": 0.0, "trade_duration": 0, "idle_duration": 1,
+ "position": 0.5, "action": 0, "reward": 0.0}]
+Path("trusted_episodes.pkl").write_bytes(pickle.dumps({"transitions": rows}))
+PY
+uv run python reward_space_analysis.py \
+ --num_samples 64 --seed 42 --skip_feature_analysis \
+ --real_episodes trusted_episodes.pkl --out_dir format_check
+```
+
+One row demonstrates loading only: it cannot support a meaningful distribution
+comparison or PBRS trajectory verification. For operational comparison, use
+trusted recorded transitions with adequate history and the relevant optional
+trajectory fields. The Python-only `enforce_columns=False` loader option fills
+missing columns with NaN; it provides no protection against malicious pickle.
+
---
## CLI Parameters
+### Boolean syntax
+
+Boolean options do not all use the same parser form:
+
+| Options | Accepted direct CLI form | Meaning |
+| --------------------------------------------------------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------- |
+| `--skip_feature_analysis`, `--skip_partial_dependence`, `--strict_diagnostics`, `--unrealized_pnl` | Bare flag, no value | Absent=false; `--flag false` is invalid. |
+| `--action_masking` | Bare flag, no value | Already true when absent; disable with `--params action_masking=false`. |
+| `--strict_validation` | Bare flag, no value | Always true in the CLI; no disable flag or accepted `--params` key. |
+| `--exit_plateau`, `--hold_potential_enabled`, `--entry_additive_enabled`, `--exit_additive_enabled` | `0` or `1` | Integer choices; direct `true`/`false` strings are invalid. |
+| `--check_invariants` | Numeric value; use `0` or `1` | Generated parser uses float despite boolean semantics; direct `true`/`false` strings are invalid. |
+
+For boolean reward parameters and `action_masking`, `--params` accepts
+`true/false`, `1/0`, `yes/no`, `y/n` and `on/off`, case-insensitively.
+Prefer `true/false` or `1/0`. Presence flags are not bulk reward parameters.
+For example:
+
+```shell
+uv run python reward_space_analysis.py --num_samples 64 --seed 42 \
+ --hold_potential_enabled 0 --skip_feature_analysis \
+ --params hold_potential_enabled=true action_masking=false --out_dir bool_override
+```
+
+This resolves hold potential to true and action masking to false. Canonical
+reward defaults are merged with explicit direct options, then `--params` takes
+highest precedence, irrespective of argument order. Repeated bulk entries for
+one key use the last value. Unknown and simulation-only bulk keys fail before
+artifacts are written.
+
### Simulation & Environment
- **`--num_samples`** (int, default: 20000) – Synthetic scenarios. More = better
synthetic PnL generation (pre-scaling). (Simulation-only).
- **`--pnl_duration_vol_scale`** (float, default: 0.5) – Additional PnL
volatility scale proportional to trade duration ratio. (Simulation-only).
-- **`--real_episodes`** (path, optional) – Episodes pickle for real vs synthetic
- distribution shift metrics. (Simulation-only; triggers additional outputs when
- provided).
+- **`--real_episodes`** (path, optional) – Trusted episodes pickle for real vs
+ synthetic distribution shift metrics. See [the schema and security warning](#trusted-pickle-inputs).
+ (Simulation-only; triggers additional outputs when provided).
- **`--unrealized_pnl`** (flag, default: false) – Track a sampled market
price separately from the retained price within each trade; map its fee-aware
PnL through duration-based tanh scaling before retaining the mark. Retained
- **`--profit_aim`** (float, default: 0.03) – Profit target threshold (e.g.
0.03=3%).
- **`--risk_reward_ratio`** (float, default: 2.0) – Risk-reward multiplier.
-- **`--action_masking`** (bool, default: true) – With masking enabled, sample
+- **`--action_masking`** (bare flag, default: true) – Disable via
+ `--params action_masking=false`. With masking enabled, sample
only valid actions. When disabled, sample an invalid action with 10% probability
and apply the configured invalid-action penalty. Invalid actions leave the held
position unchanged, except for an independent terminal liquidation.
### Diagnostics & Validation
-- **`--check_invariants`** (bool, default: true) – Enable runtime invariant
- checks (diagnostics become advisory if disabled). Toggle rarely; disabling may
- hide reward drift or invariance violations.
-- **`--strict_validation`** (flag, default: true) – Enforce parameter bounds,
- finite checks, and exact `exit_potential_mode` choices; raises when enabled.
- Relaxed API validation clamps bounds and canonicalizes an invalid exit mode with
- a recorded adjustment.
+- **`--check_invariants`** (numeric 0/1, default: true) – Enable runtime
+ invariant checks; `0` makes diagnostics advisory and can hide violations.
+- **`--strict_validation`** (bare flag, always true in CLI) – Enforce parameter
+ bounds, finite checks and exact `exit_potential_mode` choices. The CLI cannot
+ disable this. Relaxed Python API validation may clamp/canonicalize with
+ recorded adjustments; it is not available through `--params`.
- **`--strict_diagnostics`** (flag, default: false) – Raise on extreme distribution
moments instead of warning. In both modes, constants retain exact mean/std,
while undefined higher moments, normality tests and Q-Q fits remain N/A.
| Both skipped | ✓ | ✓ | No | No | Marked "(skipped)" |
Auto-skip if `num_samples < 4`.
-
+The table describes requested computation on usable data, not guaranteed results
+or file presence. Skipping PD can still produce header-only CSVs; fallback
+analysis can leave unavailable importances. See [the export contract](#data-exports).
Reusing an output directory removes only stale analyzer-owned
`feature_importance.csv` and the three `partial_dependence_{trade_duration,idle_duration,pnl}.csv`
files before writing the new report; unrelated files are retained.
### Overrides vs --params
-Direct flags and `--params` produce identical outcomes; conflicts resolved by
-bulk `--params` values.
+Valid direct options and equivalent `--params` overrides produce the same
+parameter values, but boolean syntax differs: see [Boolean syntax](#boolean-syntax).
+Bulk overrides win conflicts; a bare presence flag does not accept `false`.
```shell
uv run python reward_space_analysis.py --win_reward_factor 3.0 --idle_penalty_ratio 2.0 --num_samples 15000
### Data Exports
-| File | Description |
-| -------------------------- | ---------------------------------------------------- |
-| `reward_samples.csv` | Raw synthetic samples |
-| `feature_importance.csv` | Feature importance rankings |
-| `partial_dependence_*.csv` | Partial dependence data |
-| `manifest.json` | Runtime manifest (simulation + reward params + hash) |
+| File | Description |
+| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
+| `reward_samples.csv` | Raw synthetic samples |
+| `feature_importance.csv` | Finite importance estimates, a header-only placeholder, or fallback rows with empty importance fields (NaN); see conditions below. |
+| `partial_dependence_*.csv` | Computed curves or header-only placeholders; see conditions below. |
+| `manifest.json` | Runtime manifest (simulation + reward params + hash) |
+
+Header-only PD files are **placeholders, not computed curves**. Their header is
+`<feature>,partial_dependence`; the feature-importance placeholder header is
+`feature,importance_mean,importance_std`.
+
+| Condition | `feature_importance.csv` | PD CSVs |
+| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
+| Feature analysis explicitly skipped | Not created | Not created, regardless of the PD flag. |
+| Fewer than 4 rows, PD requested | Not created | Three header-only placeholders. |
+| Fewer than 4 rows, PD skipped | Not created | Not created. |
+| At least 4 rows, sklearn unavailable | Header-only placeholder | Three header-only placeholders, even with PD skipped. |
+| At least 4 rows, model fitted with finite importance estimates, PD skipped | Computed importance estimates | Three header-only placeholders. |
+| At least 4 rows, model fitted with finite importance estimates, PD requested | Computed importance estimates | Curves only for returned PD features; possibly none. |
+| At least 4 rows, sklearn available, no fitted model after preprocessing/split/fitting | Header-only if no usable features; otherwise rows with empty importance fields (NaN). | Three header-only placeholders if PD skipped; otherwise not created. |
+| At least 4 rows, model fitted but importance estimates unavailable | Rows with empty importance fields (NaN). | Three header-only placeholders if PD skipped; otherwise curves only for independently returned PD features, possibly none. |
+
+The three reserved PD names use `trade_duration`, `idle_duration` and `pnl`.
+At least 4 rows and scikit-learn availability are not sufficient for valid
+importance estimates. The helper drops constant and wholly NaN feature columns;
+fewer than two usable features, remaining feature NaNs, or split/fitting failure
+leave no fitted model. Empty feature sets produce header-only CSVs; retained
+features produce rows with NaN importances, serialized as empty numeric fields.
+Even with a fitted model, failed or undefined permutation importance can leave
+NaN estimates; requested PD computations are independent and may still succeed.
+
+The summary distinguishes PD placeholders from computed curves. The footer labels
+feature importance as complete only when the model is fitted and every value in
+both `importance_mean` and `importance_std` is finite. Otherwise it identifies
+unavailable estimates, retaining feature rows when present or marking a header-only
+placeholder. A complete ranking does not certify predictive validity. Existing
+analyzer-owned feature/PD artifacts are removed before a new report, as described
+under [Skipping Feature Analysis](#skipping-feature-analysis); unrelated files are
+retained. `reward_samples.csv`, the report and the manifest are separate outputs;
+the manifest is produced after reporting succeeds.
The `sample_entry_prob`, `sample_exit_prob`, and `sample_neutral_prob` columns in
`reward_samples.csv` report marginal probabilities of valid actions when applicable.
### Real Data Comparison
+Only import trusted files with [the required schema](#trusted-pickle-inputs).
+
```shell
uv run python reward_space_analysis.py \
--num_samples 100000 \
### Slow Execution
-Lower samples; skip PD/feature analysis; reduce resamples; ensure SSD.
+Lower `--num_samples`; use `--skip_partial_dependence` or
+`--skip_feature_analysis`; bound `--rf_n_jobs` and `--perm_n_jobs`. Bootstrap
+resamples are Python-API-only (`bootstrap_resamples` with declared independent
+observations), not a CLI control; the descriptive CLI does not bootstrap.
### Memory Errors
importance_df = None
analysis_stats = None
partial_deps = {}
+ partial_dependence_placeholders = False
if skip_feature_analysis or len(df) < 4:
print("CLI: Skipping feature analysis; insufficient samples or flag set")
- # Do NOT create feature_importance.csv when skipped (tests expect absence)
+ # Feature importance is unavailable when analysis is skipped.
# Create minimal partial dependence placeholders only if feature analysis was NOT explicitly skipped
if not skip_feature_analysis and not skip_partial_dependence:
+ partial_dependence_placeholders = True
for feature in ["trade_duration", "idle_duration", "pnl"]:
(output_dir / f"partial_dependence_{feature}.csv").write_text(
f"{feature},partial_dependence\n", encoding="utf-8"
)
else:
# Create empty files to keep outputs stable
+ partial_dependence_placeholders = True
for feature in ["trade_duration", "idle_duration", "pnl"]:
(output_dir / f"partial_dependence_{feature}.csv").write_text(
f"{feature},partial_dependence\n", encoding="utf-8"
)
except ImportError:
print("CLI: Skipping feature analysis; scikit-learn unavailable")
+ partial_dependence_placeholders = True
(output_dir / "feature_importance.csv").write_text(
"feature,importance_mean,importance_std\n", encoding="utf-8"
)
(output_dir / f"partial_dependence_{feature}.csv").write_text(
f"{feature},partial_dependence\n", encoding="utf-8"
)
+ computed_partial_dependence = any(not pd_df.empty for pd_df in partial_deps.values())
# Enhanced statistics
test_seed = (
f.write(header + sep + "\n".join(rows) + "\n\n")
f.write("**Exported Data:**\n")
f.write("- Full feature importance: `feature_importance.csv`\n")
- if not skip_partial_dependence:
- f.write("- Partial dependence plots: `partial_dependence_*.csv`\n\n")
- else:
+ if computed_partial_dependence:
+ f.write("- Computed partial dependence curves: `partial_dependence_*.csv`\n\n")
+ elif skip_partial_dependence:
f.write(
- "- Partial dependence plots: (skipped via --skip_partial_dependence)\n\n"
+ "- Partial dependence: header-only placeholders (--skip_partial_dependence)\n\n"
)
+ else:
+ f.write("- Partial dependence: no computed curves available\n\n")
# Section 5: Statistical Analysis
if hypothesis_tests or bootstrap_ci or dist_diagnostics or distribution_shift:
f.write("\n")
f.write("**Generated Files:**\n")
if not skip_feature_analysis and len(df) >= 4:
- f.write("- `feature_importance.csv` - Complete feature importance rankings\n")
- f.write("- `partial_dependence_*.csv` - Partial dependence data for visualization\n")
+ if importance_df is None or importance_df.empty:
+ f.write(
+ "- `feature_importance.csv` - Header-only placeholder; feature analysis unavailable\n"
+ )
+ elif (
+ analysis_stats is not None
+ and analysis_stats["model_fitted"]
+ and np.isfinite(importance_df["importance_mean"]).all()
+ and np.isfinite(importance_df["importance_std"]).all()
+ ):
+ f.write("- `feature_importance.csv` - Complete feature importance rankings\n")
+ else:
+ f.write(
+ "- `feature_importance.csv` - Importance estimates unavailable; feature rows retained\n"
+ )
+ if partial_dependence_placeholders:
+ f.write("- `partial_dependence_*.csv` - Header-only placeholders; no computed curves\n")
+ elif computed_partial_dependence:
+ f.write("- `partial_dependence_*.csv` - Computed curves for available features\n")
def main() -> None:
-# Test Docstring Template
+# Test docstring template
-Use this template as a guide when writing or updating test docstrings.
+Describe the behavior actually asserted by a test. Docstrings do not prove an
+invariant, and an example copied into another test does not create ownership.
+Use the [coverage mapping](README.md#coverage-mapping-invariant-ownership) to
+resolve a complete registered ID and its owner.
-## Standard Format
+## Standard format
-```python
-def test_feature_behavior_expected_outcome(self):
- """Brief one-line summary of what this test verifies (imperative mood).
-
- **Invariant:** [invariant-category-number] (if applicable)
-
- Extended description providing context about:
- - What behavior/property is being tested
- - Why this test is important
- - Edge cases or special conditions covered
-
- **Setup:**
- - Key parameters: profit_aim=X, risk_reward_ratio=Y
- - Test scenarios: duration_ratios=[...], modes=[...]
- - Sample size: N samples
-
- **Assertions:**
- - Primary check: What the main assertion verifies
- - Secondary checks: Additional validations (if any)
-
- **Tolerance rationale:** (if using custom tolerance)
- - [TOLERANCE.TYPE]: Reason for this tolerance choice
- Example: IDENTITY_RELAXED for accumulated errors across 5+ operations
-
- **See also:**
- - Related tests: test_other_related_feature
- - Documentation: Section 3.2 in PBRS guide
- """
- # Test implementation
- pass
-```
-
-## Quick Examples
-
-### Minimal (Simple Test)
+Use these fields when they contribute information; omit irrelevant sections:
-```python
-def test_transform_zero_input(self):
- """All potential transforms should map zero to zero."""
- for transform in TRANSFORM_MODES:
- result = apply_transform(0.0, transform)
- self.assertAlmostEqual(result, 0.0, places=12)
-```
-
-### Standard (Most Tests)
-
-```python
-def test_exit_factor_monotonic_attenuation(self):
- """Exit factor must decrease monotonically with increasing duration ratio.
+1. **Summary:** one sentence identifying the consumer-visible behavior.
+2. **Invariant / invariant reference:** a complete registered ID. Declare
+ ownership only at the mapped owner; identify non-owning checks explicitly.
+3. **Context:** the condition and why it changes the expected behavior.
+4. **Setup:** actual parameter values, regimes, seeds and boundary conditions.
+5. **Assertions:** numerical expectations, transitions, bounds or errors checked
+ by the implementation—not merely the presence of text or a section heading.
+6. **Tolerance rationale:** the named tolerance and the residual/error being
+ bounded. Do not invent accumulated floating-point error estimates.
+7. **See also:** existing owners or documentation that resolves the contract.
- **Invariant:** robustness-exit-monotonic-115
+Keep a simple test's docstring short. Use explicit setup/assertion sections for
+multi-condition tests. For regressions, state the failing condition and expected
+observable result; do not invent issue numbers or reserve an unregistered ID.
- Validates that for all attenuation modes (linear, sqrt, power, etc.),
- the exit factor decreases as duration_ratio increases, ensuring
- that longer-held positions receive progressively smaller rewards.
+## Canonical exit example
- **Setup:**
- - Attenuation modes: [linear, sqrt, power, half_life]
- - Duration ratios: [0.0, 0.5, 1.0, 1.5, 2.0]
- - PnL: 0.05, target: 0.10
-
- **Assertions:**
- - Strict monotonicity: factor[i] > factor[i+1] for all i
- - Lower bound: All factors remain non-negative
-
- **Tolerance rationale:**
- - IDENTITY_RELAXED: Exit factor computation involves normalization,
- kernel application, and optional transforms (3-5 operations)
- """
- # Test implementation
- pass
-```
-
-### Complex (Multi-Part Test)
+This example documents the existing owner
+[pbrs/test_pbrs.py](pbrs/test_pbrs.py)::`test_exit_step_shaping_matches_exit_step_rules`.
+It is a docstring example, not a second implementation to add to the suite.
```python
-def test_pbrs_terminal_state_comprehensive(self):
- """PBRS terminal potential must be zero and shaping must recover last potential.
-
- **Invariant:** pbrs-terminal-zero-201, pbrs-recovery-202
+"""Verify canonical exit releases the stored potential.
- Comprehensive validation of PBRS terminal state behavior across all
- exit potential modes (progressive_release, spike_cancel, canonical).
- Ensures theoretical PBRS guarantees hold in practice.
+**Invariant:** pbrs-canonical-exit-semantic-106
- **Background:**
- PBRS theory (Ng et al., 1999) requires terminal potential = 0 to
- maintain policy invariance. This test verifies implementation correctness.
+The exit step uses the stored previous potential rather than recomputing it
+from the exit's current PnL and duration.
- **Test structure:**
- 1. Part A: Terminal potential verification
- - For each exit mode, compute next_potential at terminal state
- - Assert: next_potential ≈ 0 within TOLERANCE.IDENTITY_RELAXED
+**Setup:**
+- Canonical mode, active hold potential, entry/exit additives disabled.
+- gamma=0.94, stored prev_potential=0.42, current PnL=0.02.
+- Current duration ratio=0.5; is_exit=True, is_entry=False.
- 2. Part B: Shaping recovery verification
- - Verify: reward_shaping ≈ -prev_potential (canonical exit)
- - Checks proper potential release at terminal
+**Assertions:**
+- next_potential=0.
+- reward_shaping=-0.42.
+- pbrs_delta=-0.42.
- 3. Part C: Trajectory identity verification
- - Verify each local shaping identity and adjacent potential continuity
- - Verify the discounted finite-trajectory boundary identity
- - Require terminal next potential to be zero
- **Setup:**
- - Exit modes: [progressive_release, spike_cancel, canonical]
- - Gamma values: [0.9, 0.95, 0.99]
- - Episodes: 100 per configuration
- - Sample size: 500 steps per episode
+**Tolerance rationale:**
+- The owning test uses DECIMAL_PLACES_STRICT for terminal zero and
+ IDENTITY_RELAXED for the shaping/delta comparisons to stored potential.
+ These bound comparison residuals; they are not a policy-invariance proof.
- **Assertions:**
- - Terminal potential: |next_potential| < TOLERANCE.IDENTITY_RELAXED
- - Local identity: shaping_t ≈ gamma * next_potential_t - prev_potential_t
- - Continuity: next_potential_t ≈ prev_potential_(t+1)
- - Discounted boundary: sum(gamma**t * shaping_t) + prev_potential_0
- - gamma**T * next_potential_(T-1) ≈ 0
-
- **Tolerance rationale:**
- - IDENTITY_RELAXED: PBRS calculations involve gamma discounting,
- potential computations (hold/entry/exit), and reward shaping formula.
- Each operation accumulates ~1e-16 error; 5-10 operations → 1e-09 bound.
-
- **See also:**
- - test_pbrs_spike_cancel_invariance: Focused spike_cancel test
- - test_pbrs_progressive_release_decay: Decay mechanism validation
- - docs/PBRS_THEORY.md: Mathematical foundations
- """
- # Part A implementation
- for mode in EXIT_MODES:
- # ...
-
- # Part B implementation
- # ...
-
- # Part C implementation
- # ...
+**See also:**
+- Coverage Mapping in tests/README.md.
+"""
```
-## Section Guidelines
-
-### **One-Line Summary**
-
-- Use imperative mood: "Verify X does Y", "Check that A equals B"
-- Be specific: "Exit factor decreases monotonically" not "Test exit factor"
-- Focus on **what** is tested, not **how** it's tested
-
-### **Invariant** (if applicable)
-
-- Format: `**Invariant:** category-name-number`
-- Example: `**Invariant:** pbrs-terminal-zero-201`
-- See `tests/helpers/assertions.py` for invariant documentation
-
-### **Extended Description**
-
-- Explain **why** this test exists
-- Provide context about the feature being tested
-- Mention edge cases or special conditions
-
-### **Setup**
-
-- Key parameters and their values
-- Test scenarios (modes, ratios, sample sizes)
-- Any special configuration
-
-### **Assertions**
-
-- What each major assertion validates
-- Expected relationships or properties
-- Bounds and thresholds
-
-### **Tolerance Rationale**
-
-- **Required** if using non-default tolerance
-- Explain accumulated error sources
-- Justify the specific tolerance magnitude
-- See `constants.py` docstrings for available tolerances
-
-### **See Also**
-
-- Related tests
-- Relevant documentation
-- Theory references
-
-## Common Patterns
-
-### Property-Based Test
-
-```python
-def test_property_holds_for_all_inputs(self):
- """Property X holds for all valid inputs in domain D.
-
- Property-based test verifying [property] across [input space].
- Uses parameterized inputs to ensure comprehensive coverage.
- """
-```
-
-### Regression Test
-
-```python
-def test_issue_123_regression_is_prevented(self):
- """Regression test for Issue #123: [brief description].
-
- Validates behavior for [conditions] and asserts the expected outcome.
-
- **Tracking:** Issue #123
- """
-```
-
-### Integration Test
-
-```python
-def test_end_to_end_workflow_integration(self):
- """End-to-end integration test for [workflow].
-
- Validates complete workflow from [start] to [end], including:
- - Component A: [responsibility]
- - Component B: [responsibility]
- - Component C: [responsibility]
-
- **Integration points:**
- - A → B: [interface/data flow]
- - B → C: [interface/data flow]
- """
-```
-
-## Anti-Patterns to Avoid
-
-### ❌ Vague One-Liners
-
-```python
-def test_reward_calculation(self):
- """Test reward calculation.""" # Too vague!
-```
-
-### ❌ Implementation Details
-
-```python
-def test_uses_numpy_vectorization(self):
- """Test uses numpy for speed.""" # Focus on behavior, not implementation
-```
-
-### ❌ Missing Tolerance Justification
-
-```python
-def test_complex_calculation(self):
- """Test complex multi-step calculation."""
- result = complex_function(...)
- self.assertAlmostEqual(result, expected, delta=TOLERANCE.IDENTITY_RELAXED)
- # ❌ Why IDENTITY_RELAXED? Should explain!
-```
-
-### ❌ Overly Technical Jargon
-
-```python
-def test_l2_norm_convergence(self):
- """Test that the L2 norm of the gradient converges under SGD."""
- # ❌ Unless testing ML internals, use domain language
-```
-
-## Checklist for New Tests
-
-- [ ] One-line summary is clear and specific
-- [ ] Invariant listed (if applicable)
-- [ ] Extended description explains why test exists
-- [ ] Setup section documents key parameters
-- [ ] Assertions section explains what's validated
-- [ ] Tolerance rationale provided (if custom tolerance used)
-- [ ] References to related tests/docs (if applicable)
-- [ ] Test name follows convention: `test_category_behavior_outcome`
-- [ ] Docstring uses proper markdown formatting
-- [ ] No implementation details leaked into description
-
-## References
-
-- **Test Naming:** Follow pytest conventions and project standards
-- **Invariant Documentation:** See `tests/helpers/assertions.py`
-- **Tolerance Selection:** See `tests/constants.py` for available tolerances
-- **Test Organization:** See `tests/README.md`
-
----
-
-**Note:** Not all sections are required for every test. Simple tests can use minimal format.
-Complex tests should use comprehensive format with all relevant sections.
+## PBRS mode and trajectory limits
+
+Do not generalize the canonical exit rule to every exit mode. For an exit with
+stored potential 2, gamma=0.95 and decay=0.5, the current helper's next potentials
+are:
+
+| Mode | Next potential |
+| ------------------- | ------------------------------------------------------ |
+| canonical | 0 |
+| non_canonical | 0; additive terms can change canonical classification. |
+| progressive_release | 1 |
+| spike_cancel | 2 / 0.95 |
+| retain_previous | 2 |
+
+An exit is not automatically a terminal transition. A test that requires zero
+terminal potential must use a mode/path that actually imposes it. Name the
+applicable configuration and distinguish additive reward from shaping.
+
+For a complete ordered trajectory, verify the local identity
+`F_t = gamma * next_potential_t - prev_potential_t`, adjacent potential
+continuity, and the finite discounted boundary:
+
+`sum(gamma**t * F_t) = -prev_potential_0 + gamma**T * next_potential_(T-1)`.
+
+Canonical verification additionally requires the observed final next potential
+to be zero and the applicable additive terms to be absent. For gamma below 1,
+the undiscounted sum of shaping is not generally zero. Passing these checks
+supports only the trajectories and assumptions exercised; it does not establish
+universal policy invariance. Refer to the real PBRS tests and the
+[analyzer's PBRS configuration](../README.md#pbrs-configuration) for the current
+contracts and diagnostic classification.
- PBRS shaping mechanics (canonical exit semantics, near-zero classification)
- Robustness under extreme / invalid parameter settings
- Statistical metrics integrity (bootstrap, constant distributions)
-- CLI parameter propagation & report formatting
+- CLI input validation and artifact lifecycle
- Cross-component smoke scenarios
Single ownership per invariant is tracked in the Coverage Mapping section of
## Taxonomy Directories
-| Directory | Marker | Scope |
-| -------------- | ----------- | ------------------------------------------- |
-| `components/` | components | Component math |
-| `transforms/` | transforms | Mathematical transform functions |
-| `robustness/` | robustness | Edge cases, stability, progression |
-| `api/` | api | Public API helpers & parsing |
-| `cli/` | cli | CLI parameter propagation & artifacts |
-| `pbrs/` | pbrs | Potential-based shaping invariance & modes |
-| `statistics/` | statistics | Statistical metrics, tests, bootstrap |
-| `integration/` | integration | Smoke scenarios & report formatting |
-| `helpers/` | (none) | Helper utilities (data loading, assertions) |
+| Directory | Marker | Scope |
+| -------------- | ----------- | --------------------------------------------- |
+| `components/` | components | Component math |
+| `transforms/` | transforms | Mathematical transform functions |
+| `robustness/` | robustness | Edge cases, stability, progression |
+| `api/` | api | Public API helpers & parsing |
+| `cli/` | cli | CLI parameter propagation & artifacts |
+| `pbrs/` | pbrs | Potential-based shaping invariance & modes |
+| `statistics/` | statistics | Statistical metrics, tests, bootstrap |
+| `integration/` | integration | Cross-component reward behavior and workflows |
+| `helpers/` | (none) | Helper utilities (data loading, assertions) |
Markers are declared in `pyproject.toml` and enforced with `--strict-markers`.
- Tolerance rationale (required for non-default tolerances)
- See also (related tests/docs)
-**Template provides three complexity levels** (minimal, standard, complex) with
-examples for property-based tests, regression tests, and integration tests.
+The template gives field guidelines and a real canonical-exit docstring example,
+including PBRS mode and trajectory limits.
### Markers
stable across unrelated line insertions.
- Notes: Sub-modes, non-owning references and multi-path coverage.
-| ID | Category | Description | Owning test | Notes |
-| --------------------------------------------- | ----------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| report-additives-deterministic-092 | components | Additives deterministic report section | components/test_additives.py::test_additive_activation_deterministic_contribution | Integration/PBRS may reference outcome non-owning |
-| robustness-decomposition-integrity-101 | robustness | Single active core component equals total reward under mutually exclusive scenarios | robustness/test_robustness.py::test_decomposition_integrity | Scenarios: idle, hold, exit, invalid; non-owning refs integration/test_reward_calculation.py |
-| robustness-exit-mode-fallback-102 | robustness | Unknown exit_attenuation_mode falls back to linear w/ warning | robustness/test_robustness.py::test_robustness_102_unknown_exit_mode_fallback_linear | |
-| robustness-negative-grace-clamp-103 | robustness | Negative exit_plateau_grace clamps to 0.0 w/ warning | robustness/test_robustness.py::test_robustness_103_negative_plateau_grace_clamped | |
-| robustness-invalid-power-tau-104 | robustness | Invalid power tau falls back alpha=1.0 w/ warning | robustness/test_robustness.py::test_robustness_104_invalid_power_tau_fallback_alpha_one | |
-| robustness-near-zero-half-life-105 | robustness | Near-zero half life yields no attenuation (factor≈base) | robustness/test_robustness.py::test_robustness_105_half_life_near_zero_fallback | |
-| pbrs-canonical-exit-semantic-106 | pbrs | Canonical exit uses shaping=-prev_potential and next_potential=0.0 | pbrs/test_pbrs.py::test_exit_step_shaping_matches_exit_step_rules | Uses stored potential across steps; no drift correction applied |
-| statistics-partial-deps-skip-107 | statistics | skip_partial_dependence => empty PD structures | statistics/test_statistics.py::test_statistics_feature_analysis_skip_partial_dependence | |
-| helpers-transitions-preserve-multiplicity-108 | helpers | Repeated transitions retain their empirical multiplicity | helpers/test_utilities.py::test_repeated_transitions_preserve_multiplicity | |
-| helpers-missing-cols-fill-109 | helpers | Missing required columns filled with NaN + single warning | helpers/test_utilities.py::test_missing_multiple_required_columns_single_warning | |
-| statistics-binned-stats-min-edges-110 | statistics | <2 bin edges raises ValueError | statistics/test_statistics.py::test_statistics_binned_stats_invalid_bins_raises | Docstring line |
-| statistics-constant-cols-exclusion-111 | statistics | Constant columns excluded & listed | statistics/test_statistics.py::test_statistics_correlation_dropped_constant_columns | Docstring line |
-| statistics-degenerate-distribution-shift-112 | statistics | Constants: zero distances; KS p only with declared independent observations | statistics/test_statistics.py::test_statistics_distribution_shift_metrics_degenerate_zero | Docstring line |
-| statistics-constant-dist-exact-ci-113a | statistics | Both modes retain exact constant CI bounds | statistics/test_statistics.py::test_stats_bootstrap_constant_distribution_exact_bounds | |
-| statistics-percentile-outside-mean-113b | statistics | Percentile bounds need not contain the sample mean | statistics/test_statistics.py::test_stats_bootstrap_percentiles_need_not_contain_mean | |
-| statistics-constant-diagnostics-115 | statistics | Constants have N/A higher moments, normality tests and Q-Q fits in both modes | statistics/test_statistics.py::test_statistics_distribution_constant_diagnostics | |
-| pbrs-canonical-near-zero-report-116 | pbrs | Canonical trajectories with valid evidence are classified as verified | pbrs/test_pbrs.py::test_pbrs_canonical_near_zero_report | Requires local identity, continuity, discounted terminal boundary, and zero observed additives; the non-owning boundary test also covers a complete singleton terminal episode |
-| robustness-exit-pnl-only-117 | robustness | Only exit actions have non-zero PnL | robustness/test_robustness.py::test_pnl_invariant_exit_only | |
-| pbrs-absence-shift-placeholder-118 | pbrs | Placeholder shift line present when shaping shift is absent | pbrs/test_pbrs.py::test_pbrs_absence_and_distribution_shift_placeholder | |
-| components-pbrs-breakdown-fields-119 | components | PBRS breakdown fields finite and mathematically aligned | components/test_reward_components.py::test_pbrs_breakdown_fields_finite_and_aligned | Tests base_reward, pbrs_delta and invariance_correction alignment |
-| integration-pbrs-metrics-section-120 | integration | PBRS Metrics section present in report with tracing metrics | integration/test_report_formatting.py::test_report_includes_pbrs_metrics_section | |
-| cli-pbrs-csv-columns-121 | cli | PBRS columns in reward_samples.csv when shaping enabled | cli/test_cli_params_and_csv.py::test_csv_contains_pbrs_columns_when_shaping_present | Verifies finite reward_base, reward_pbrs_delta and reward_invariance_correction values |
-| statistics-bh-finite-family-122 | statistics | Undefined tests excluded from finite-only BH family; marked non-applicable | statistics/test_statistics.py::test_bh_excludes_undefined_tests_from_finite_family | |
-| statistics-independence-contract-123 | statistics | Inferential helpers require independent_observations=True | statistics/test_statistics.py::test_inference_helpers_require_independent_observations | Covers hypothesis tests and bootstrap intervals |
-| report-independent-sections-124 | integration | CI, diagnostics and shift sections do not depend on hypothesis-test output | integration/test_report_formatting.py::test_statistical_sections_do_not_depend_on_hypothesis_tests | Also verifies the reported bootstrap resample count |
-| pbrs-discounted-evidence-125 | pbrs | Verification requires local identity, continuity and discounted terminal boundary | pbrs/test_pbrs.py::test_pbrs_canonical_discontinuous_potentials_report | Discontinuous potentials are not verified |
-| statistics-proportional-histograms-126 | statistics | Proportional histograms ignore sample count; moved mass yields positive KL/JS | statistics/test_statistics.py::test_distribution_shift_proportional_histograms_ignore_sample_count | KL and JS remain finite and non-negative |
-| pbrs-near-bound-clamp-127 | pbrs | Relaxed near-bound clamps apply exact endpoints and retain all reasons | pbrs/test_pbrs.py::test_validate_reward_parameters_records_near_bound_clamps_exactly | Includes numeric-string coercion |
-| pbrs-exit-mode-validation-128 | pbrs | Exit-potential choices are strict or canonicalized; direct calls fail safe | pbrs/test_pbrs.py::test_invalid_exit_mode_warns_at_direct_and_simulation_boundaries | Direct calculation and simulation boundaries warn before fallback; PBRS calls suppress additives |
-| cli-invalid-exit-mode-129 | cli | Invalid `--params exit_potential_mode` fails before artifacts | cli/test_cli_params_and_csv.py::test_invalid_exit_potential_mode_params_fails_before_artifacts | Strict CLI validation |
-| cli-warning-header-recognition-130 | cli | Warning counts accept only anchored Python warning header formats | cli/test_cli_params_and_csv.py::test_warning_header_positive_and_negative_formats | Covers POSIX, relative, synthetic and Windows source locations |
-| pbrs-invalid-mode-provenance-131 | pbrs | Invalid imported exit-mode metadata cannot certify canonical invariance | pbrs/test_pbrs.py::test_report_rejects_invalid_exit_mode_provenance | Preserves the invalid raw value and reports effective additive settings as unknown |
-| pbrs-synthetic-fee-floor-132 | pbrs | High-fee entry loss remains in long/short synthetic PnL and exit rewards | pbrs/test_pbrs.py::test_synthetic_fee_loss_extrema_match_retained_pnl | Direct and transformed trajectories |
-| pbrs-synthetic-profitable-mark-133 | pbrs | Favorable long/short marks remain profitable after high-fee synthetic transformation | pbrs/test_pbrs.py::test_synthetic_high_fee_winner_retains_profitable_mark | Covers immediate gains and recovery after an initial loss on the independent sampled market path |
-| pbrs-synthetic-fee-boundary-134 | pbrs | Fee-boundary rounding is accepted while materially extreme PnL is rejected | pbrs/test_pbrs.py::test_synthetic_high_fee_short_boundary_rejects_real_excess | Non-unit entry price exposes floating-point roundoff |
-| pbrs-synthetic-latent-price-135 | pbrs | Clipped candidate PnL does not erase the sampled market path for later holds | pbrs/test_pbrs.py::test_unrealized_pnl_retains_sampled_market_path_after_candidate_cap | Equal first retained marks from distinct market prices diverge after the same adverse return |
-| api-unmasked-invalid-actions-136 | api | Unmasked invalid actions keep positions and receive penalties | api/test_api_helpers.py::test_unmasked_simulation_samples_invalid_actions_without_changing_position | Includes spot, futures and zero-penalty invalid actions |
-| api-terminal-invalid-exit-137 | api | Wrong-side terminal exits liquidate the held side | api/test_api_helpers.py::test_invalid_terminal_exit_liquidates_the_held_position | Prevents dropped terminal trades |
-| cli-stale-generated-artifacts-138 | cli | Skipped analyses remove only reserved outputs from an earlier run | cli/test_cli_params_and_csv.py::test_skipped_analysis_removes_only_stale_generated_artifacts | Preserves unrelated files |
-| helpers-nonfinite-episodes-139 | helpers | Infinite real observations become missing without dropping rows | helpers/test_utilities.py::test_nonfinite_numeric_episodes_are_marked_missing | Retains transition multiplicity |
-| statistics-finite-shift-140 | statistics | Finite observations remain comparable despite an infinite value | statistics/test_statistics.py::test_distribution_shift_uses_finite_observations | Rejects all-nonfinite features |
-| statistics-rank-direction-141 | statistics | Rank-biserial effect follows the named first-group advantage | statistics/test_statistics.py::test_pnl_rank_biserial_direction_matches_named_first_group | Checks both directions |
-| statistics-bootstrap-count-142 | statistics | Zero and negative resample counts fail for variable and constant data | statistics/test_statistics.py::test_bootstrap_rejects_nonpositive_resample_count | Rejects missing bootstrap |
-| api-unmasked-sample-probabilities-144 | api | Unmasked sample probabilities match marginal valid-action frequencies | api/test_api_helpers.py::test_unmasked_sampling_probabilities_match_action_frequencies | Spot/futures entries, long/short exits, and neutral probability |
+| ID | Category | Description | Owning test | Notes |
+| --------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| report-additives-deterministic-092 | components | Additives deterministic report section | components/test_additives.py::test_additive_activation_deterministic_contribution | Integration/PBRS may reference outcome non-owning |
+| robustness-decomposition-integrity-101 | robustness | Single active core component equals total reward under mutually exclusive scenarios | robustness/test_robustness.py::test_decomposition_integrity | Scenarios: idle, hold, exit, invalid; non-owning refs integration/test_reward_calculation.py |
+| robustness-exit-mode-fallback-102 | robustness | Unknown exit_attenuation_mode falls back to linear w/ warning | robustness/test_robustness.py::test_robustness_102_unknown_exit_mode_fallback_linear | |
+| robustness-negative-grace-clamp-103 | robustness | Negative exit_plateau_grace clamps to 0.0 w/ warning | robustness/test_robustness.py::test_robustness_103_negative_plateau_grace_clamped | |
+| robustness-invalid-power-tau-104 | robustness | Invalid power tau falls back alpha=1.0 w/ warning | robustness/test_robustness.py::test_robustness_104_invalid_power_tau_fallback_alpha_one | |
+| robustness-near-zero-half-life-105 | robustness | Near-zero half life yields no attenuation (factor≈base) | robustness/test_robustness.py::test_robustness_105_half_life_near_zero_fallback | |
+| pbrs-canonical-exit-semantic-106 | pbrs | Canonical exit uses shaping=-prev_potential and next_potential=0.0 | pbrs/test_pbrs.py::test_exit_step_shaping_matches_exit_step_rules | Uses stored potential across steps; no drift correction applied |
+| statistics-partial-deps-skip-107 | statistics | skip_partial_dependence => empty PD structures | statistics/test_statistics.py::test_statistics_feature_analysis_skip_partial_dependence | |
+| helpers-transitions-preserve-multiplicity-108 | helpers | Repeated transitions retain their empirical multiplicity | helpers/test_utilities.py::test_repeated_transitions_preserve_multiplicity | |
+| helpers-missing-cols-fill-109 | helpers | Missing required columns filled with NaN + single warning | helpers/test_utilities.py::test_missing_multiple_required_columns_single_warning | |
+| statistics-binned-stats-min-edges-110 | statistics | <2 bin edges raises ValueError | statistics/test_statistics.py::test_statistics_binned_stats_invalid_bins_raises | Docstring line |
+| statistics-constant-cols-exclusion-111 | statistics | Constant columns excluded & listed | statistics/test_statistics.py::test_statistics_correlation_dropped_constant_columns | Docstring line |
+| statistics-degenerate-distribution-shift-112 | statistics | Constants: zero distances; KS p only with declared independent observations | statistics/test_statistics.py::test_statistics_distribution_shift_metrics_degenerate_zero | Docstring line |
+| statistics-constant-dist-exact-ci-113a | statistics | Both modes retain exact constant CI bounds | statistics/test_statistics.py::test_stats_bootstrap_constant_distribution_exact_bounds | |
+| statistics-percentile-outside-mean-113b | statistics | Percentile bounds need not contain the sample mean | statistics/test_statistics.py::test_stats_bootstrap_percentiles_need_not_contain_mean | |
+| statistics-constant-diagnostics-115 | statistics | Constants have N/A higher moments, normality tests and Q-Q fits in both modes | statistics/test_statistics.py::test_statistics_distribution_constant_diagnostics | |
+| pbrs-canonical-near-zero-report-116 | pbrs | Canonical trajectories with valid evidence are classified as verified | pbrs/test_pbrs.py::test_pbrs_canonical_near_zero_report | Requires local identity, continuity, discounted terminal boundary, and zero observed additives; the non-owning boundary test also covers a complete singleton terminal episode |
+| robustness-exit-pnl-only-117 | robustness | Only exit actions have non-zero PnL | robustness/test_robustness.py::test_pnl_invariant_exit_only | |
+| pbrs-absence-shift-placeholder-118 | pbrs | Placeholder shift line present when shaping shift is absent | pbrs/test_pbrs.py::test_pbrs_absence_and_distribution_shift_placeholder | |
+| components-pbrs-breakdown-fields-119 | components | PBRS breakdown fields finite and mathematically aligned | components/test_reward_components.py::test_pbrs_breakdown_fields_finite_and_aligned | Tests base_reward, pbrs_delta and invariance_correction alignment |
+| cli-pbrs-csv-columns-121 | cli | PBRS columns in reward_samples.csv when shaping enabled | cli/test_cli_params_and_csv.py::test_csv_contains_pbrs_columns_when_shaping_present | Verifies finite reward_base, reward_pbrs_delta and reward_invariance_correction values |
+| statistics-bh-finite-family-122 | statistics | Undefined tests excluded from finite-only BH family; marked non-applicable | statistics/test_statistics.py::test_bh_excludes_undefined_tests_from_finite_family | |
+| statistics-independence-contract-123 | statistics | Inferential helpers require independent_observations=True | statistics/test_statistics.py::test_inference_helpers_require_independent_observations | Covers hypothesis tests and bootstrap intervals |
+| pbrs-discounted-evidence-125 | pbrs | Verification requires local identity, continuity and discounted terminal boundary | pbrs/test_pbrs.py::test_pbrs_canonical_discontinuous_potentials_report | Discontinuous potentials are not verified |
+| statistics-proportional-histograms-126 | statistics | Proportional histograms ignore sample count; moved mass yields positive KL/JS | statistics/test_statistics.py::test_distribution_shift_proportional_histograms_ignore_sample_count | KL and JS remain finite and non-negative |
+| pbrs-near-bound-clamp-127 | pbrs | Relaxed near-bound clamps apply exact endpoints and retain all reasons | pbrs/test_pbrs.py::test_validate_reward_parameters_records_near_bound_clamps_exactly | Includes numeric-string coercion |
+| pbrs-exit-mode-validation-128 | pbrs | Exit-potential choices are strict or canonicalized; direct calls fail safe | pbrs/test_pbrs.py::test_invalid_exit_mode_warns_at_direct_and_simulation_boundaries | Direct calculation and simulation boundaries warn before fallback; PBRS calls suppress additives |
+| cli-invalid-exit-mode-129 | cli | Invalid `--params exit_potential_mode` fails before artifacts | cli/test_cli_params_and_csv.py::test_invalid_exit_potential_mode_params_fails_before_artifacts | Strict CLI validation |
+| cli-warning-header-recognition-130 | cli | Warning counts accept only anchored Python warning header formats | cli/test_cli_params_and_csv.py::test_warning_header_positive_and_negative_formats | Covers POSIX, relative, synthetic and Windows source locations |
+| pbrs-invalid-mode-provenance-131 | pbrs | Invalid imported exit-mode metadata cannot certify canonical invariance | pbrs/test_pbrs.py::test_report_rejects_invalid_exit_mode_provenance | Preserves the invalid raw value and reports effective additive settings as unknown |
+| pbrs-synthetic-fee-floor-132 | pbrs | High-fee entry loss remains in long/short synthetic PnL and exit rewards | pbrs/test_pbrs.py::test_synthetic_fee_loss_extrema_match_retained_pnl | Direct and transformed trajectories |
+| pbrs-synthetic-profitable-mark-133 | pbrs | Favorable long/short marks remain profitable after high-fee synthetic transformation | pbrs/test_pbrs.py::test_synthetic_high_fee_winner_retains_profitable_mark | Covers immediate gains and recovery after an initial loss on the independent sampled market path |
+| pbrs-synthetic-fee-boundary-134 | pbrs | Fee-boundary rounding is accepted while materially extreme PnL is rejected | pbrs/test_pbrs.py::test_synthetic_high_fee_short_boundary_rejects_real_excess | Non-unit entry price exposes floating-point roundoff |
+| pbrs-synthetic-latent-price-135 | pbrs | Clipped candidate PnL does not erase the sampled market path for later holds | pbrs/test_pbrs.py::test_unrealized_pnl_retains_sampled_market_path_after_candidate_cap | Equal first retained marks from distinct market prices diverge after the same adverse return |
+| api-unmasked-invalid-actions-136 | api | Unmasked invalid actions keep positions and receive penalties | api/test_api_helpers.py::test_unmasked_simulation_samples_invalid_actions_without_changing_position | Includes spot, futures and zero-penalty invalid actions |
+| api-terminal-invalid-exit-137 | api | Wrong-side terminal exits liquidate the held side | api/test_api_helpers.py::test_invalid_terminal_exit_liquidates_the_held_position | Prevents dropped terminal trades |
+| cli-stale-generated-artifacts-138 | cli | Skipped analyses remove only reserved outputs from an earlier run | cli/test_cli_params_and_csv.py::test_skipped_analysis_removes_only_stale_generated_artifacts | Preserves unrelated files |
+| helpers-nonfinite-episodes-139 | helpers | Infinite real observations become missing without dropping rows | helpers/test_utilities.py::test_nonfinite_numeric_episodes_are_marked_missing | Retains transition multiplicity |
+| statistics-finite-shift-140 | statistics | Finite observations remain comparable despite an infinite value | statistics/test_statistics.py::test_distribution_shift_uses_finite_observations | Rejects all-nonfinite features |
+| statistics-rank-direction-141 | statistics | Rank-biserial effect follows the named first-group advantage | statistics/test_statistics.py::test_pnl_rank_biserial_direction_matches_named_first_group | Checks both directions |
+| statistics-bootstrap-count-142 | statistics | Zero and negative resample counts fail for variable and constant data | statistics/test_statistics.py::test_bootstrap_rejects_nonpositive_resample_count | Rejects missing bootstrap |
+| api-unmasked-sample-probabilities-144 | api | Unmasked sample probabilities match marginal valid-action frequencies | api/test_api_helpers.py::test_unmasked_sampling_probabilities_match_action_frequencies | Spot/futures entries, long/short exits, and neutral probability |
+| statistics-report-inference-projection-146 | statistics | Constant confidence intervals require declared independence, even with an empty hypothesis map | statistics/test_statistics.py::test_report_projects_constant_intervals_only_with_declared_independence | Numeric projection; True → False → default rewrites the same report; non-owning bounds reference: statistics-constant-dist-exact-ci-113a |
+| statistics-report-feature-availability-147 | statistics | Complete importance exports require a fitted model and finite means and standard deviations | statistics/test_statistics.py::test_report_certifies_feature_importance_only_from_finite_estimates | Real finite-zero, single-feature, empty-feature and one-test-row states; numerical CSV and availability. |
### Non-Owning Smoke / Reference Checks
The following tests also check outcomes owned elsewhere:
-| Non-owning test | Reference check | Owning test |
-| ------------------------------------------------------------------------------ | ------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
-| integration/test_reward_calculation.py::test_reward_component_activation_smoke | Core component activation and decomposition | robustness/test_robustness.py::test_decomposition_integrity |
-| components/test_reward_components.py::test_exit_factor_calculation | Exit factor modes and plateau behavior | robustness/test_robustness.py::test_exit_factor_comprehensive; test_plateau_continuity_at_grace_boundary |
-| pbrs/test_pbrs.py::test_pbrs_canonical_near_zero_report | Canonical classification and decomposition | robustness/test_robustness.py::test_decomposition_integrity |
-| pbrs/test_pbrs.py::test_pbrs_non_canonical_full_report_reason_aggregation | Non-canonical report reasons | robustness/test_robustness.py::test_decomposition_integrity |
-| pbrs/test_pbrs.py::test_pbrs_non_canonical_mode_only_reason | Non-canonical exit mode without additives | robustness/test_robustness.py::test_decomposition_integrity |
-| statistics/test_statistics.py::test_stats_mean_decomposition_consistency | Mean decomposition consistency | robustness/test_robustness.py::test_decomposition_integrity |
+| Non-owning test | Reference check | Owning test |
+| ------------------------------------------------------------------------------------------------------ | ------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
+| integration/test_reward_calculation.py::test_reward_component_activation_smoke | Core component activation and decomposition | robustness/test_robustness.py::test_decomposition_integrity |
+| components/test_reward_components.py::test_exit_factor_calculation | Exit factor modes and plateau behavior | robustness/test_robustness.py::test_exit_factor_comprehensive; test_plateau_continuity_at_grace_boundary |
+| pbrs/test_pbrs.py::test_pbrs_canonical_near_zero_report | Canonical classification and decomposition | robustness/test_robustness.py::test_decomposition_integrity |
+| pbrs/test_pbrs.py::test_pbrs_non_canonical_full_report_reason_aggregation | Non-canonical report reasons | robustness/test_robustness.py::test_decomposition_integrity |
+| pbrs/test_pbrs.py::test_pbrs_non_canonical_mode_only_reason | Non-canonical exit mode without additives | robustness/test_robustness.py::test_decomposition_integrity |
+| statistics/test_statistics.py::test_stats_mean_decomposition_consistency | Mean decomposition consistency | robustness/test_robustness.py::test_decomposition_integrity |
+| statistics/test_statistics.py::test_report_projects_constant_intervals_only_with_declared_independence | Exact point-mass interval bounds | statistics/test_statistics.py::test_stats_bootstrap_constant_distribution_exact_bounds |
### Deprecated / Reserved IDs
-| ID | Status | Rationale |
-| --- | ---------- | --------------------------------------------------------------------- |
-| 093 | deprecated | CLI invariance consolidated; no dedicated test yet |
-| 094 | deprecated | CLI encoding/data migration removed in refactor |
-| 095 | deprecated | Report CLI propagation assertions merged into test_cli_params_and_csv |
-| 114 | reserved | Gap retained for potential future statistics invariant |
+| ID | Status | Rationale |
+| --- | ---------- | -------------------------------------------------------------------------------------------------------------- |
+| 093 | deprecated | CLI invariance consolidated; no dedicated test yet |
+| 094 | deprecated | CLI encoding/data migration removed in refactor |
+| 095 | deprecated | Report CLI propagation assertions merged into test_cli_params_and_csv |
+| 114 | reserved | Gap retained for potential future statistics invariant |
+| 120 | reserved | No active behavior invariant; prose-only report checks are not ownership. |
+| 124 | reserved | No prose owner; numeric confidence-interval projection is owned by statistics-report-inference-projection-146. |
## Adding New Invariants
## Duplication Audit
-Each invariant shortname must appear in exactly one taxonomy directory path:
+Use the **complete invariant ID** and its Coverage Mapping row, not a shortname
+or the number of matching directory paths. From the repository root:
```shell
cd ReforceXY/reward_space_analysis/tests
-grep -R "<shortname>" -n .
+invariant=pbrs-canonical-exit-semantic-106
+grep -R -n -F --include='*.py' -- "$invariant" .
```
-Expect a single directory path. Examples:
+Resolve the mapped owner
+`pbrs/test_pbrs.py::test_exit_step_shaping_matches_exit_step_rules` and inspect
+its actual assertions. Inline IDs/ownership declarations are optional in the
+existing suite, so no search hit (grep status 1) does not invalidate that mapping.
+Classify each hit as an owning declaration or non-owning reference; documentation
+mentions and reserved entries are not additional owners.
-```shell
-grep -R "near_zero" -n .
-grep -R "pbrs_delta" -n .
-```
+Check one registered mapping owner per active ID, the owner's behavior against
+the invariant statement, and any competing ownership declarations. Multi-path
+coverage and non-owning smoke checks are allowed when identified by the mapping,
+its notes or the Non-Owning Smoke / Reference Checks table. A shortname such as
+`near_zero` or `pbrs_delta` spans unrelated IDs and cannot prove duplication.
+Do not delete a valid reference because it is in another taxonomy directory.
## Coverage Parity Notes
-Detailed assertions reside in targeted directories (components, robustness)
-while integration tests focus on report formatting. The mapping above defines
-current ownership; multi-path and non-owning references are called out explicitly.
+Detailed numerical assertions reside in their targeted directories. Integration
+checks exercise cross-component behavior; matching report text is not numerical
+correctness. The mapping defines current ownership and allowed non-owning or
+multi-path coverage.
## When to Run Tests
FEATURE_SMALL_5: Seed for small feature test (5)
FEATURE_SMALL_3: Seed for small feature test (3)
- # Report formatting seeds
- REPORT_FORMAT_1: Seed for report formatting test 1 (234)
- REPORT_FORMAT_2: Seed for report formatting test 2 (321)
+ # PBRS trajectory seed
+ REPORT_FORMAT_2: Seed for deterministic PBRS trajectory sweeps (321)
# Additional seeds for various test scenarios
ALTERNATE_1: Alternate seed for robustness tests (555)
FEATURE_SMALL_5: int = 5
FEATURE_SMALL_3: int = 3
- # Report formatting seeds
- REPORT_FORMAT_1: int = 234
+ # PBRS trajectory seed
REPORT_FORMAT_2: int = 321
# Additional seeds
SAMPLE_SIZE_MEDIUM: Medium sample size for standard tests (400)
SAMPLE_SIZE_LARGE: Large sample size for statistical power (800)
SAMPLE_SIZE_CONST_DF: Sample size for constant dataframes (64)
+ SAMPLE_SIZE_FEATURE_UNDEFINED: Four rows leave one test row and undefined R² (4)
SAMPLE_SIZE_SHIFT_SCALE: Sample size for shift/scale tests (256)
PBRS_SIMULATION_STEPS: Number of steps for PBRS simulation tests (500)
MONTE_CARLO_ITERATIONS: Monte Carlo simulation iterations (160)
PBRS_SWEEP_ITERATIONS: Number of iterations for PBRS sweep tests (120)
BOOTSTRAP_MINIMAL_ITERATIONS: Minimal bootstrap iterations for quick tests (25)
BOOTSTRAP_EXTENDED_ITERATIONS: Extended bootstrap iterations (200)
- SAMPLE_SIZE_REPORT_MINIMAL: Minimal sample size for report smoke tests (10)
- REPORT_DURATION_SCALE_UP: Duration scale applied to synthetic real episodes (1.01)
- REPORT_DURATION_SCALE_DOWN: Duration scale applied to synthetic real episodes (0.99)
- REPORT_PNL_MEAN_SHIFT: PnL mean shift applied to synthetic real episodes (0.001)
# API smoke parameters
API_MAX_IDLE_DURATION_CANDLES: Idle duration cap used in _sample_action tests (20)
SAMPLE_SIZE_MEDIUM: int = 400
SAMPLE_SIZE_LARGE: int = 800
SAMPLE_SIZE_CONST_DF: int = 64
+ SAMPLE_SIZE_FEATURE_UNDEFINED: int = 4
SAMPLE_SIZE_SHIFT_SCALE: int = 256
# Specialized test scenario sizes
PBRS_SWEEP_ITERATIONS: int = 120
BOOTSTRAP_MINIMAL_ITERATIONS: int = 25
BOOTSTRAP_EXTENDED_ITERATIONS: int = 200
- SAMPLE_SIZE_REPORT_MINIMAL: int = 10
- REPORT_DURATION_SCALE_UP: float = 1.01
- REPORT_DURATION_SCALE_DOWN: float = 0.99
- REPORT_PNL_MEAN_SHIFT: float = 0.001
# API smoke parameters
API_MAX_IDLE_DURATION_CANDLES: int = 20
#!/usr/bin/env python3
-"""Utility tests narrowed to data loading behaviors.
-
-Moved tests:
-- Report formatting invariants -> integration/test_report_formatting.py
-- Additives deterministic contribution -> components/test_additives.py
-- CLI CSV + params propagation -> cli/test_cli_params_and_csv.py
-"""
+"""Regression checks for data loading and numerical helper behavior."""
import pickle
import unittest
+++ /dev/null
-#!/usr/bin/env python3
-"""Report formatting focused tests moved from helpers/test_utilities.py."""
-
-import re
-import unittest
-
-import numpy as np
-import pandas as pd
-import pytest
-
-from reward_space_analysis import PBRS_INVARIANCE_TOL, write_complete_statistical_analysis
-
-from ..constants import (
- PARAMS,
- SCENARIOS,
- SEEDS,
-)
-from ..test_base import RewardSpaceTestBase
-
-pytestmark = pytest.mark.integration
-
-
-class TestReportFormatting(RewardSpaceTestBase):
- def test_statistical_sections_do_not_depend_on_hypothesis_tests(self):
- """Diagnostics and bootstrap sections survive when no hypothesis tests qualify."""
- # Construct df with idle_duration always zero -> reward_idle all zeros so idle_mask.sum()==0
- # Position has only one unique value -> groups<2
- # pnl all zeros so no positive/negative groups with >=30 each
- n = SCENARIOS.SAMPLE_SIZE_TINY
- df = pd.DataFrame(
- {
- "reward": np.zeros(n),
- "reward_idle": np.zeros(n),
- "reward_hold": np.zeros(n),
- "reward_exit": np.zeros(n),
- "pnl": np.zeros(n),
- "trade_duration": np.ones(n),
- "idle_duration": np.zeros(n),
- "position": np.zeros(n),
- }
- )
- content = self._write_report(df, independent_observations=True, skip_feature_analysis=True)
- self.assertNotIn("### 5.1 Hypothesis Tests", content)
- self.assertIn("### 5.2 Confidence Intervals", content)
- self.assertIn("### 5.3 Distribution Diagnostics", content)
- self.assertIn("N/A (constant distribution)", content)
- self.assertIn(f"{SCENARIOS.SAMPLE_SIZE_SMALL:,} resamples", content)
- descriptive = self._write_report(df, real_df=df.copy(), skip_feature_analysis=True)
- self.assertNotIn("### 5.2 Confidence Intervals", descriptive)
- self.assertNotIn("Normal? (Shapiro-Wilk)", descriptive)
- self.assertIn("### 5.4 Distribution Shift Analysis", descriptive)
- self.assertNotIn("_Not performed (no real episodes provided)._", descriptive)
- self.assertIn("inference suppressed", descriptive)
-
- def _write_report(
- self, df: pd.DataFrame, *, real_df: pd.DataFrame | None = None, **kwargs
- ) -> str:
- """Helper: invoke write_complete_statistical_analysis into temp dir and return content."""
- out_dir = self.output_path / "report_tmp"
- # Ensure required columns present (action required for summary stats)
- required_cols = [
- "action",
- "reward_invalid",
- "reward_shaping",
- "reward_entry_additive",
- "reward_exit_additive",
- "duration_ratio",
- "idle_ratio",
- ]
- df = df.copy()
- for col in required_cols:
- if col not in df.columns:
- if col == "action":
- df[col] = 0.0
- else:
- df[col] = 0.0
- write_complete_statistical_analysis(
- df=df,
- output_dir=out_dir,
- profit_aim=PARAMS.PROFIT_AIM,
- risk_reward_ratio=PARAMS.RISK_REWARD_RATIO,
- seed=SEEDS.BASE,
- real_df=real_df,
- independent_observations=kwargs.get("independent_observations", False),
- adjust_method="none",
- strict_diagnostics=False,
- bootstrap_resamples=SCENARIOS.SAMPLE_SIZE_SMALL, # keep test fast
- skip_partial_dependence=kwargs.get("skip_partial_dependence", False),
- skip_feature_analysis=kwargs.get("skip_feature_analysis", False),
- )
- report_path = out_dir / "statistical_analysis.md"
- return report_path.read_text(encoding="utf-8")
-
- def test_distribution_shift_section_present_with_real_episodes(self):
- """Distribution Shift section renders metrics table when real episodes provided."""
- # Synthetic df (ensure >=10 non-NaN per feature)
- synth_df = self.make_stats_df(n=SCENARIOS.SAMPLE_SIZE_TINY, seed=SEEDS.REPORT_FORMAT_1)
- # Real df: shift slightly (different mean) so metrics non-zero
- real_df = synth_df.copy()
- real_df["pnl"] = real_df["pnl"] + SCENARIOS.REPORT_PNL_MEAN_SHIFT # small mean shift
- real_df["trade_duration"] = real_df["trade_duration"] * SCENARIOS.REPORT_DURATION_SCALE_UP
- real_df["idle_duration"] = real_df["idle_duration"] * SCENARIOS.REPORT_DURATION_SCALE_DOWN
- content = self._write_report(synth_df, real_df=real_df)
- # Assert metrics header and at least one feature row
- self.assertIn("### 5.4 Distribution Shift Analysis", content)
- self.assertIn(
- "| Feature | KL Div | JS Dist | Wasserstein | KS Stat | KS p-value |", content
- )
- # Ensure placeholder text absent
- self.assertNotIn("_Not performed (no real episodes provided)._", content)
- # Basic regex to find a feature row (pnl)
- m = re.search(r"\| pnl \| ([0-9]+\.[0-9]{4}) \| ([0-9]+\.[0-9]{4}) \|", content)
- self.assertIsNotNone(
- m, "pnl feature row missing or misformatted in distribution shift table"
- )
-
- def test_partial_dependence_redundancy_note_emitted(self):
- """Redundancy note appears when both feature analysis and partial dependence skipped."""
- df = self.make_stats_df(
- n=SCENARIOS.SAMPLE_SIZE_REPORT_MINIMAL, seed=SEEDS.REPORT_FORMAT_2
- ) # small but >=4 so skip_feature_analysis flag drives behavior
- content = self._write_report(
- df,
- real_df=None,
- skip_feature_analysis=True,
- skip_partial_dependence=True,
- )
- self.assertIn(
- "_Note: --skip_partial_dependence is redundant when feature analysis is skipped._",
- content,
- )
- # Ensure feature importance section shows skipped label
- self.assertIn("Feature Importance - (skipped)", content)
- # Ensure no partial dependence plots line for success path appears
- self.assertNotIn("partial_dependence_*.csv", content)
-
- # Owns invariant: integration-pbrs-metrics-section-120
- def test_report_includes_pbrs_metrics_section(self):
- """Verify statistical_analysis.md includes PBRS Metrics section with tracing metrics.
-
- Verifies:
- - PBRS Metrics subsection exists when PBRS columns present
- - Section includes Mean Base Reward, Mean PBRS Term, Mean Invariance Correction
- - All metrics are formatted with proper precision
- """
- # Create df with PBRS columns
- n = SCENARIOS.SAMPLE_SIZE_SMALL
- rng = np.random.default_rng(SEEDS.REPORT_FORMAT_1)
- df = pd.DataFrame(
- {
- "reward": rng.normal(0, 0.1, n),
- "reward_invalid": np.zeros(n),
- "reward_idle": np.zeros(n),
- "reward_hold": np.zeros(n),
- "reward_exit": rng.normal(0, 0.05, n),
- "reward_shaping": rng.normal(0, 0.02, n),
- "reward_entry_additive": np.zeros(n),
- "reward_exit_additive": np.zeros(n),
- # PBRS columns
- "reward_base": rng.normal(0, 0.1, n),
- "reward_pbrs_delta": rng.normal(0, 0.02, n),
- "reward_invariance_correction": rng.normal(0, PBRS_INVARIANCE_TOL / 10.0, n),
- "pnl": rng.normal(0, 0.01, n),
- "trade_duration": rng.integers(10, 100, n).astype(float),
- "idle_duration": np.zeros(n),
- "position": rng.choice([0, 1, 2], n).astype(float),
- "action": rng.choice([0, 1, 2, 3, 4], n).astype(float),
- "duration_ratio": rng.uniform(0, 1, n),
- "idle_ratio": np.zeros(n),
- }
- )
-
- content = self._write_report(df)
-
- # Verify PBRS Metrics section exists
- self.assertIn("**PBRS Metrics:**", content)
-
- # Verify key metrics are present
- required_metrics = [
- "Mean Base Reward",
- "Std Base Reward",
- "Mean PBRS Delta",
- "Std PBRS Delta",
- "Mean Invariance Correction",
- "Std Invariance Correction",
- "Max \\|Invariance Correction\\|",
- "Mean \\|PBRS\\| / \\|Base\\| Ratio",
- ]
-
- for metric in required_metrics:
- self.assertIn(metric, content, f"Missing metric in PBRS Metrics section: {metric}")
-
- # Verify proper formatting (values should be formatted with proper precision)
- # Check for at least one properly formatted metric line
- m = re.search(r"\| Mean Base Reward \| (-?[0-9]+\.[0-9]{6}) \|", content)
- self.assertIsNotNone(m, "Mean Base Reward metric missing or misformatted")
-
-
-if __name__ == "__main__":
- unittest.main()
independent_observations=False,
)
+ def test_report_projects_constant_intervals_only_with_declared_independence(self):
+ """Project constant intervals only after independence is declared.
+
+ Owns statistics-report-inference-projection-146. Zero-PnL exits with
+ constant shaping have no eligible hypothesis tests. Rewriting the same
+ report with False or the undeclared default must remove its intervals.
+ Point-mass bounds are a non-owning reference to
+ statistics-constant-dist-exact-ci-113a; no rounding tolerance is needed.
+ """
+ expected_means = {
+ "reward": -PARAMS.BASE_FACTOR,
+ "reward_idle": 0.0,
+ "reward_hold": 0.0,
+ "reward_exit": 0.0,
+ "pnl": 0.0,
+ "reward_shaping": -PARAMS.BASE_FACTOR,
+ "reward_entry_additive": 0.0,
+ "reward_exit_additive": 0.0,
+ }
+ df = pd.DataFrame(
+ {
+ **expected_means,
+ "reward_invalid": 0.0,
+ "trade_duration": PARAMS.TRADE_DURATION_SHORT,
+ "idle_duration": 0.0,
+ "position": reward_space_analysis.Positions.Long.value,
+ "action": reward_space_analysis.Actions.Long_exit.value,
+ "duration_ratio": PARAMS.TRADE_DURATION_SHORT
+ / self.DEFAULT_PARAMS["max_trade_duration_candles"],
+ "idle_ratio": 0.0,
+ },
+ index=range(SCENARIOS.SAMPLE_SIZE_CONST_DF),
+ )
+ for declaration in (True, False, None):
+ with self.subTest(independent_observations=declaration):
+ inference_kwargs = (
+ {} if declaration is None else {"independent_observations": declaration}
+ )
+ reward_space_analysis.write_complete_statistical_analysis(
+ df,
+ self.output_path,
+ profit_aim=PARAMS.PROFIT_AIM,
+ risk_reward_ratio=PARAMS.RISK_REWARD_RATIO,
+ seed=SEEDS.BASE,
+ bootstrap_resamples=SCENARIOS.BOOTSTRAP_EXTENDED_ITERATIONS,
+ skip_feature_analysis=True,
+ **inference_kwargs,
+ )
+ content = (self.output_path / "statistical_analysis.md").read_text(encoding="utf-8")
+ rows = []
+ for line in content.splitlines():
+ cells = [cell.strip() for cell in line.split("|")[1:-1]]
+ if len(cells) == 5 and cells[0] in expected_means:
+ rows.append((cells[0], *(float(cell) for cell in cells[1:])))
+ expected = (
+ [(metric, mean, mean, mean, 0.0) for metric, mean in expected_means.items()]
+ if declaration is True
+ else []
+ )
+ self.assertCountEqual(rows, expected)
+
+ def test_report_certifies_feature_importance_only_from_finite_estimates(self):
+ """Report availability must agree with real fit state and numerical exports.
+
+ Owns statistics-report-feature-availability-147. Constant exit shaping
+ gives finite zero importances with a normal holdout, but undefined
+ permutation estimates with one test row. A single varying predictor or
+ no varying predictors leaves no fitted model. No exact prose is pinned.
+ """
+ cases = (
+ ("finite", SCENARIOS.SAMPLE_SIZE_CONST_DF, True, True),
+ ("single_feature", SCENARIOS.SAMPLE_SIZE_CONST_DF, False, False),
+ ("empty_features", SCENARIOS.SAMPLE_SIZE_CONST_DF, False, False),
+ ("undefined", SCENARIOS.SAMPLE_SIZE_FEATURE_UNDEFINED, True, False),
+ )
+ for case, count, fitted, complete in cases:
+ with self.subTest(case=case):
+ pnl = np.linspace(-PARAMS.PNL_SMALL, PARAMS.PNL_SMALL, count)
+ duration = np.linspace(0.0, PARAMS.TRADE_DURATION_SHORT, count)
+ if case == "single_feature":
+ duration[:] = PARAMS.TRADE_DURATION_SHORT
+ elif case == "empty_features":
+ pnl[:] = 0.0
+ duration[:] = PARAMS.TRADE_DURATION_SHORT
+ df = pd.DataFrame(
+ {
+ "reward": -PARAMS.BASE_FACTOR,
+ "reward_shaping": -PARAMS.BASE_FACTOR,
+ "reward_invalid": 0.0,
+ "reward_idle": 0.0,
+ "reward_hold": 0.0,
+ "reward_exit": 0.0,
+ "reward_entry_additive": 0.0,
+ "reward_exit_additive": 0.0,
+ "pnl": pnl,
+ "trade_duration": duration,
+ "idle_duration": 0.0,
+ "position": reward_space_analysis.Positions.Long.value,
+ "action": reward_space_analysis.Actions.Long_exit.value,
+ "duration_ratio": duration
+ / self.DEFAULT_PARAMS["max_trade_duration_candles"],
+ "idle_ratio": 0.0,
+ }
+ )
+ importance, stats, _, model = reward_space_analysis._perform_feature_analysis(
+ df, SEEDS.BASE, skip_partial_dependence=True, rf_n_jobs=1, perm_n_jobs=1
+ )
+ self.assertEqual(stats["model_fitted"], fitted)
+ self.assertEqual(model is not None, fitted)
+ self.assertEqual(importance.empty, case == "empty_features")
+ estimates = importance[["importance_mean", "importance_std"]].to_numpy()
+ if complete:
+ np.testing.assert_array_equal(estimates, np.zeros_like(estimates))
+ elif not importance.empty:
+ self.assertTrue(np.isnan(estimates).all())
+ reward_space_analysis.write_complete_statistical_analysis(
+ df,
+ self.output_path,
+ PARAMS.PROFIT_AIM,
+ PARAMS.RISK_REWARD_RATIO,
+ SEEDS.BASE,
+ skip_partial_dependence=True,
+ rf_n_jobs=1,
+ perm_n_jobs=1,
+ )
+ exported = pd.read_csv(self.output_path / "feature_importance.csv")
+ pd.testing.assert_frame_equal(
+ exported, importance, check_dtype=False, check_exact=True
+ )
+ content = (self.output_path / "statistical_analysis.md").read_text(encoding="utf-8")
+ entry = next(
+ line
+ for line in content.splitlines()
+ if line.startswith("- `feature_importance.csv`")
+ )
+ self.assertEqual("complete" in entry.lower(), complete)
+ self.assertEqual("unavailable" in entry.lower(), not complete)
+
def test_stats_bootstrap_rejects_invalid_bounds(self):
"""Non-finite or reversed bounds are errors, never repaired by the validator."""
for bounds in (
--- /dev/null
+"""Config-template parser regressions; requires the matching Freqtrade QA image."""
+
+import unittest
+from pathlib import Path
+
+from scripts.config_template_contract import (
+ assert_optional_ccxt_rate_limits_load_without_delimiter_edits,
+)
+
+
+class ConfigTemplateTest(unittest.TestCase):
+ def test_optional_ccxt_rate_limits_load_without_delimiter_edits(self):
+ template = Path(__file__).resolve().parents[1] / "user_data/config-template.json"
+ assert_optional_ccxt_rate_limits_load_without_delimiter_edits(self, template)
-"""Regressions for live observations, HPO options and historic prediction alignment."""
+"""Regressions for configuration, observations, HPO and prediction alignment."""
import copy
import math
"walletAddress": "",
"privateKey": "",
"ccxt_config": {
- "enableRateLimit": true,
- // "rateLimit": 60
+ // "rateLimit": 60,
+ "enableRateLimit": true
},
"ccxt_async_config": {
- "enableRateLimit": true,
- // "rateLimit": 60
+ // "rateLimit": 60,
+ "enableRateLimit": true
},
// Spot top 5
- "pair_whitelist": [
- "BTC/USDT",
- "ETH/USDT",
- "SOL/USDT",
- "BNB/USDT",
- "XRP/USDT"
- ],
+ "pair_whitelist": ["BTC/USDT", "ETH/USDT", "SOL/USDT", "BNB/USDT", "XRP/USDT"]
// // Spot IA
// "pair_whitelist": [
// "NEAR/USDT",
"data_kitchen_thread_count": 4, // set to number of CPU threads / 4
"track_performance": false,
"feature_parameters": {
- "include_corr_pairlist": [
- "BTC/USDT",
- "ETH/USDT"
- ],
- "include_timeframes": [
- "5m",
- "15m",
- "1h",
- "4h"
- ],
+ "include_corr_pairlist": ["BTC/USDT", "ETH/USDT"],
+ "include_timeframes": ["5m", "15m", "1h", "4h"],
"include_shifted_candles": 6,
"weight_factor": 0,
"principal_component_analysis": false,
"use_SVM_to_remove_outliers": false,
"use_DBSCAN_to_remove_outliers": false,
- "indicator_periods_candles": [
- 8,
- 16,
- 32
- ],
+ "indicator_periods_candles": [8, 16, 32],
"inlier_metric_window": 0,
"noise_standard_deviation": 0.02,
"reverse_train_test_order": false,
class ReforceXY(BaseReinforcementLearningModel):
"""
Custom Freqtrade Freqai reinforcement learning prediction model.
- Model specific config:
+ User configuration: ReforceXY/README.md. Example fragment:
{
"freqaimodel": "ReforceXY",
"strategy": "RLAgentStrategy",
"max_no_improvement_evals": 0, // Maximum consecutive evaluations without a new best model
"min_evals": 0, // Number of evaluations before start to count evaluations without improvements
"check_envs": true, // Check that an environment follows Gym API
- "tensorboard_throttle": 1, // Number of training calls between tensorboard logs
+ "tensorboard_throttle": 1, // Collect info metrics when aggregate num_timesteps is divisible by this value
"plot_new_best": false, // Enable tensorboard rollout plot upon finding a new best model
"plot_window": 2000, // Environment history window used for tensorboard rollout plot
},
"enabled": false, // Enable hyperopt
"n_trials": 100, // Number of trials
"n_startup_trials": 15, // Number of initial random trials for TPESampler
- "timeout_hours": 0, // Maximum time in hours for hyperopt (0 = no timeout)
- "continuous": false, // If true, perform continuous optimization
+ "timeout_hours": 0, // Soft HPO hours checked between trials; final fit/evaluation excluded (0 disables)
+ "continuous": false, // If true, delete/recreate the study on each optimize call
"warm_start": false, // If true, enqueue previous best params if exists
"sampler": "tpe", // Optuna sampler (tpe|auto)
"storage": "sqlite", // Optuna storage backend (sqlite|file)
--- /dev/null
+# Strategy evaluation protocol
+
+[Operator guides](../README.md#documentation) describe how to run each strategy.
+This protocol governs evidence for changing configurations or strategy behavior.
+
+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-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
+[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/
+[stationary-bootstrap]: https://doi.org/10.1080/01621459.1994.10476870
--- /dev/null
+# QuickAdapter
+
+QuickAdapterV3 uses QuickAdapterRegressorV3 to predict smoothed Zigzag extrema
+morphology, not returns. Regressor error is a model diagnostic, not a profit score.
+
+## Contents
+
+- [Quick start](#quick-start)
+- [Decision cycle](#decision-cycle)
+- [Supplied profile versus runtime fallbacks](#supplied-profile-versus-runtime-fallbacks)
+- [Models and HPO](#models-and-hpo)
+- [Configuration formats](#configuration-formats)
+- [Continual learning](#continual-learning)
+- [Prediction warmup and restarts](#prediction-warmup-and-restarts)
+- [Configuration reference](#configuration-reference)
+
+## Quick start
+
+From the repository root:
+
+```shell
+cd quickadapter
+cp user_data/config-template.json user_data/config.json
+```
+
+Review the exchange, pairs, sizing and `freqai` configuration in
+`user_data/config.json`; keep dry-run enabled while checking behavior. Add private
+exchange credentials only for operations that require them. Review the timezone
+in [docker-compose.yml](docker-compose.yml) and the
+[API/security and maintenance guidance](../README.md#start-safely).
+
+```shell
+docker compose up -d --build
+```
+
+The build follows the moving `stable_freqai` base. Record its resolved digest
+and installed dependency versions when comparing runs.
+
+## Decision cycle
+
+1. FreqAI builds features, trains the chosen regressor on labeled historical
+ data and supplies an `&s-extrema` prediction for each decision candle.
+2. A long entry requires `do_predict=1`, `DI_catch=1` and
+ `&s-extrema < minima_threshold`. A short entry instead requires
+ `&s-extrema > maxima_threshold`, with the same validity gates. Equality does
+ not trigger an entry. Adaptive thresholds need the real-prediction warmup
+ described below.
+3. Entry confirmation also checks the current order price for the configured
+ volatility-adjusted reversal, available history, global capacity and per-side
+ capacity. A prediction signal alone does not guarantee a trade. Spot rejects
+ shorts; margin/futures can allow them. With both sides allowed, an odd finite
+ global capacity is rounded up before halving for each side; the global limit
+ still applies. `max_open_trades=-1` means unlimited capacity.
+4. Open trades use the custom volatility-based stoploss, which never loosens,
+ including after partial exits. The strategy stoploss is -2.5%; leverage,
+ execution and exchange conditions affect the actual outcome.
+5. Three take-profit stages request 40%, 30% and 20% of the stake remaining at
+ each stage. Exchange minimums can turn a partial request into a full close.
+ The final target arms a persisted trailing full exit: a material adverse
+ retracement exits, not elapsed stagnation alone. Open orders block new staged
+ adjustments.
+6. Model expiration (`do_predict=2`) requests an exit. A valid opposite extrema
+ signal requests an exit only after price-reversal confirmation. Outlier
+ predictions are counted per candle; that count is not itself an unconditional
+ exit rule. Custom safety and stoploss exits remain separate from entry signals.
+
+Inspect prediction status, thresholds, confirmation logs and open exposure when
+no entries occur. Do not disable safety gates merely to increase trade count.
+
+## Supplied profile versus runtime fallbacks
+
+[config-template.json](user_data/config-template.json) is the supplied profile,
+not a copy of every runtime fallback. An explicit configured value takes
+precedence; the reference below describes the fallback when a value is absent
+or falls back through its documented validation. Review normalized startup logs
+and Freqtrade overrides for the effective configuration.
+
+The template enables dry-run and spot trading on 5m candles. It selects XGBoost,
+21-day training windows, 2-day backtest prediction windows, a 0.333 holdout,
+Kaiser–Bessel-derived label smoothing (window 6, beta 10), Isodata thresholding,
+and HPO with 100 trials and 6 workers. These differ intentionally from runtime
+fallbacks such as holdout 0.1 with `train_test_split`, Gaussian smoothing (window 5, beta 8), mean
+thresholding and HPO disabled (50 trials, 1 worker when enabled).
+
+The template explicitly sets `freqai.fit_live_predictions_candles=864`; its
+runtime fallback is 100. The strategy startup candle count follows this value.
+At 5m, 864 candles span a nominal 72 hours; that is an observation horizon, not
+a promise of readiness 72 wall-clock hours after startup. Initial training,
+missing predictions, expired models and history gaps affect readiness.
+
+`label_prediction.method=none` does not choose another calibration algorithm:
+it skips adaptive minima/maxima threshold computation. With the supplied static
+thresholds -2/+2, ordinary threshold-driven entries normally do not occur.
+Independently configured static thresholds can permit them; stoploss and model
+expiration exits are not disabled by this setting.
+
+## Models and HPO
+
+`freqai.regressor` selects one of `xgboost`, `lightgbm`,
+`histgradientboostingregressor`, `ngboost` or `catboost`. All run through the
+same strategy decision/threshold workflow; this is not a performance ranking.
+Use model-specific training parameters supported by the selected backend.
+
+`freqai.optuna_hyperopt.enabled=false` disables regressor and dynamic label
+HPO. When enabled, trial, worker and timeout budgets apply. Label HPO selects
+morphology parameters, not held-out trading profit. `holdout_rmse` reports
+weighted error on the original label scale before deployment refit; unavailable
+holdouts produce infinity. Use the [evaluation protocol](../docs/evaluation.md)
+for economic comparison and the limitations of threshold replay in native
+backtests. Changing fitted pipelines, label weighting or study schemas can
+require a new `freqai.identifier` or an intentional reset; see the reference.
+
+## Configuration formats
+
+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.<glob>`. 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.
+
+## 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.
+
+## Prediction warmup and restarts
+
+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.
+
+## Configuration reference
+
+Runtime validation and canonical fallback definitions live in
+[Utils.py](user_data/strategies/Utils.py) and
+[QuickAdapterRegressorV3.py](user_data/freqaimodels/QuickAdapterRegressorV3.py).
+This guide covers QuickAdapter behavior, overrides and SDK boundaries. The
+shipped JSON profile is the authoritative example of explicit overrides. For
+standard framework settings, use the [Freqtrade configuration reference][freqtrade-configuration]
+and the [FreqAI parameter reference][freqai-parameters].
+
+- [Protections](#protections)
+- [Leverage](#leverage)
+- [Exit pricing](#exit-pricing)
+- [Reversal confirmation](#reversal-confirmation)
+- [Regressor model](#regressor-model)
+- [Model training parameters](#model-training-parameters)
+- [Data split parameters](#data-split-parameters)
+- [Label smoothing](#label-smoothing)
+- [Label weighting](#label-weighting)
+- [Label pipeline](#label-pipeline)
+- [Feature parameters](#feature-parameters)
+- [Label prediction](#label-prediction)
+- [Optuna / HPO](#optuna--hpo)
+
+### Protections
+
+| Path | Runtime fallback | Type / Range | Description |
+| ------------------------------------------------- | ---------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 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
+
+| Path | Runtime fallback | Type / Range | Description |
+| -------- | ------------------- | ------------------------- | ------------------------------------------------------- |
+| leverage | `proposed_leverage` | float [1.0, max_leverage] | Leverage. Fallback to `proposed_leverage` for the pair. |
+
+### Exit pricing
+
+| Path | Runtime fallback | Type / Range | Description |
+| --------------------------------------------------- | ---------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 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
+
+| Path | Runtime fallback | Type / Range | Description |
+| -------------------------------------------------- | ---------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 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
+
+| Path | Runtime fallback | Type / Range | Description |
+| ----------------------------------- | ---------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| freqai.fit_live_predictions_candles | 100 | int >= 1 | Required real-prediction calibration window per pair; also strategy startup candles and protection lookback basis. The template explicitly supplies 864. See [prediction warmup](#prediction-warmup-and-restarts). |
+| 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
+
+| Path | Runtime fallback | Type / Range | Description |
+| -------------------------------------------- | ---------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 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
+
+| Path | Runtime fallback | Type / Range | Description |
+| ------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 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 (`train_test_split`); dynamic (`timeseries_split`, key omitted) | float [0,1) \| int >= 0 | 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. Omitting `test_size` with `timeseries_split` uses sklearn dynamic outer sizing; inner validation then falls back to `0.1`. Explicitly supplied Freqtrade configuration values must be numeric. |
+| 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
+
+| Path | Runtime fallback | Type / Range | Description |
+| ------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 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
+
+| Path | Runtime fallback | Type / Range | Description |
+| --------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| 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 * <fill_epsilon_baseline>(pivot_weights)`; `gaussian` applies per-pivot Gaussian bumps; `epsilon_gaussian` sums the `epsilon` floor and the `gaussian` bumps. Pivot rows take the max of their raw weight and the off-pivot field at their index (no-op for `zero`). 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
+
+| Path | Runtime fallback | Type / Range | Description |
+| ----------------------------------------- | ---------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 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`=Yeo–Johnson followed by centering and unit-variance scaling fitted on the transformed finite training labels. |
+| 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
+
+| Path | Runtime fallback | Type / Range | Description |
+| -------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 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<label_horizon_candles`; (3) for `train_test_split`, applies the same fixed `label_horizon_candles` purge around the train/test boundary; (4) both split methods additionally drop any train row whose label-aware availability reaches the test boundary; (5) label weighting becomes causal: the `epsilon` baseline at each row uses only pivot weights available with that row's label. `false` is deprecated: the causal split-guard rejections are lifted, but the toggle-independent ones remain (`timeseries_split` still rejects `shuffle_after_split`, and an active holdout still rejects all three at evaluation); `timeseries_split` `gap` auto-sets from `label_period_candles`, and Gaussian fills keep unbounded tails. |
+| freqai.feature_parameters.min_label_period_candles | 12 | int >= 1 | Minimum labeling NATR period used for reversals labeling HPO. |
+| freqai.feature_parameters.max_label_period_candles | 24 | int >= 1 | Maximum labeling NATR period used for reversals labeling HPO. |
+| freqai.feature_parameters.label_natr_multiplier | min/max midpoint | float > 0 | Zigzag labeling NATR multiplier. |
+| freqai.feature_parameters.min_label_natr_multiplier | 9.0 | float > 0 | Minimum labeling NATR multiplier used for reversals labeling HPO. |
+| freqai.feature_parameters.max_label_natr_multiplier | 12.0 | float > 0 | Maximum labeling NATR multiplier used for reversals labeling HPO. |
+| freqai.feature_parameters.label_frequency_candles | `auto` | int [2, 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
+
+| Path | Runtime fallback | Type / Range | Description |
+| ------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| freqai.label_prediction.method | `thresholding` | enum {`none`,`thresholding`} | `thresholding` computes adaptive thresholds. `none` skips calibration; the supplied ±2 static thresholds normally prevent threshold-driven entries. It is not an alternative entry strategy. Independently supplied static thresholds can change that result; safety/stoploss exits remain available. |
+| 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
+
+| Path | Runtime fallback | Type / Range | Description |
+| ----------------------------------------------------------- | ---------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 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`. |
+
+[freqtrade-configuration]: https://www.freqtrade.io/en/stable/configuration/
+[freqai-parameters]: https://www.freqtrade.io/en/stable/freqai-parameter-table/#general-configuration-parameters
+[knn-density]: https://doi.org/10.1214/aoms/1177700079
+[silverman-density]: https://doi.org/10.1201/9781315140919
--- /dev/null
+"""Config-template parser regressions; requires the matching Freqtrade QA image."""
+
+import unittest
+from pathlib import Path
+
+from scripts.config_template_contract import (
+ assert_optional_ccxt_rate_limits_load_without_delimiter_edits,
+)
+
+
+class ConfigTemplateTest(unittest.TestCase):
+ def test_optional_ccxt_rate_limits_load_without_delimiter_edits(self):
+ template = Path(__file__).resolve().parents[1] / "user_data/config-template.json"
+ assert_optional_ccxt_rate_limits_load_without_delimiter_edits(self, template)
"walletAddress": "",
"privateKey": "",
"ccxt_config": {
+ // "rateLimit": 60,
"enableRateLimit": true
- // "rateLimit": 60
},
"ccxt_async_config": {
+ // "rateLimit": 60,
"enableRateLimit": true
- // "rateLimit": 60
},
// Spot top 5
"pair_whitelist": ["BTC/USDT", "ETH/USDT", "SOL/USDT", "BNB/USDT", "XRP/USDT"]
--- /dev/null
+"""Shared native config-template contract for the strategy test suites."""
+
+import copy
+import json
+import re
+import tempfile
+import unittest
+from pathlib import Path
+
+from freqtrade.configuration.load_config import load_config_file
+
+
+def assert_optional_ccxt_rate_limits_load_without_delimiter_edits(
+ test: unittest.TestCase, template: Path
+) -> None:
+ """Only uncommenting selected rateLimit examples may change the parsed config."""
+ source = template.read_text(encoding="utf-8")
+ baseline = load_config_file(str(template))
+ sections = ("ccxt_config", "ccxt_async_config")
+ example = re.compile(r'(?m)^[ \t]*(?P<marker>//)[ \t]*"rateLimit"\s*:\s*')
+ decoder = json.JSONDecoder()
+ for section in sections:
+ test.assertNotIn("rateLimit", baseline["exchange"][section])
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / "config-template.json"
+ for enabled in ((), sections[:1], sections[1:], sections):
+ with test.subTest(enabled=enabled):
+ text = source
+ expected = copy.deepcopy(baseline)
+ for section in enabled:
+ start = re.search(rf'"{section}"\s*:\s*\{{', text)
+ if start is None:
+ test.fail(f"Missing CCXT section {section}")
+ match = example.search(text, start.end())
+ if match is None:
+ test.fail(f"Missing commented rateLimit example for {section}")
+ value, _ = decoder.raw_decode(text[match.end() :])
+ marker = match.start("marker")
+ text = text[:marker] + text[marker + 2 :]
+ expected["exchange"][section]["rateLimit"] = value
+ path.write_text(text, encoding="utf-8")
+ test.assertEqual(load_config_file(str(path)), expected)