--- /dev/null
-Run each suite in its matching Freqtrade QA image, with the repository mounted
-at `/workspace` and `/workspace` as the working directory:
+ # 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
+
-# ReforceXY
-python -m unittest discover -s ReforceXY/tests -v
-
-# QuickAdapter
-PYTHONPATH=/workspace/quickadapter/user_data/strategies \
- python -m unittest discover -s quickadapter/tests -v
++Run a strategy's suite inside its matching Freqtrade QA image, with the checkout
++mounted at `/workspace` and that directory as the working directory. Use the
++shared runner for canonical discovery and the coverage gate:
+
+ ```shell
-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:
++# Inside the ReforceXY QA image; select quickadapter inside its own QA image.
++strategy=ReforceXY
++export PYTHONPATH=/workspace/$strategy/user_data/strategies
++export COVERAGE_RCFILE=$strategy/.coveragerc COVERAGE_FILE=/tmp/.coverage
++sh scripts/run-coverage.sh -s "$strategy/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.
+
-```shell
-PYTHONPATH=quickadapter/user_data/strategies \
- python -m unittest discover -s quickadapter/tests -k 'test_utils_zigzag.*' -v
-```
++Both coverage commands need the same explicit configuration and data path; the
++runner propagates test and coverage-report failures. QuickAdapter needs the
++strategy path for bare-name imports; it is optional for ReforceXY.
+
-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.
++For a focused debug run without coverage, use `python -m unittest discover` with
++the same suite path and an exact `module.Class.method` pattern, for example
++`-k 'test_model_pbrs_transitions.*' -v` for ReforceXY. A pattern matching no tests
++fails with exit code 5. This debug run does not establish the 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
++Each strategy QA matrix entry runs its type check and canonical coverage suite.
++ReforceXY additionally runs once with `FREQAI_QA_SHUFFLE_SEED=1`. This permutes
++methods within test classes, not module or class order. `QaTestCase` restores the
++action-mask cache and Python, NumPy and CPU Torch RNG states before and after
++each test. Lifecycle probes cover successful cases, body failures and setup or
++teardown errors; each test still owns isolation of any other mutable state.
++The reward-space analysis suite runs separately with `uv`, without a Freqtrade
++image.
+
+ ## Coverage gate
+
-sh scripts/run-coverage.sh -s quickadapter/tests -v
-```
++Both strategy suites enforce their own runtime coverage gates. ReforceXY
++measures the complete `ReforceXY/user_data` tree with a 70% minimum, including
++branches and non-imported namespace files. Tests, the analytical package and
++QuickAdapter do not contribute to that denominator. The standalone analysis
++suite has a separate 85% gate; see [its testing documentation](ReforceXY/reward_space_analysis/tests/README.md).
+
-the pull request. `quickadapter/tests/test_coverage_floor.py` refuses a
++Each strategy's `.coveragerc` is selected explicitly because coverage.py does
++not search parent directories. Set `strategy=quickadapter` in its matching QA
++image and use the same canonical runner command above for its unchanged gate.
+
+ 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 BasedPyright and type-stub versions are pinned in each project's
-`.devcontainer/requirements-dev.txt`, as is `coverage` in QuickAdapter's.
++the pull request. Each strategy's `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, type-stub and coverage versions are pinned in each project's
++`.devcontainer/requirements-dev.txt`.
+ 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.
--- /dev/null
-`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.
+ # 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,
-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.
++`do_predict=2` (expired model) requests exits only for the current pair's open
++positions, using each position's side independently of the predicted action.
++Other pairs cannot generate its exit signals. 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.
+
++An absent configured `gamma` uses the canonical discount. The effective discount
++must be a finite Python/NumPy integer or floating scalar in `[0, 1]`, normalized
++to a Python float. Null, booleans, strings, complex values and arrays are rejected
++before constructing or replacing environments. Validation follows HPO overrides
++and the resumed learner's gamma, so a valid higher-priority value can supersede
++an invalid base value. A persisted learner with missing or invalid gamma cannot
++continue training. Learner and environment use the same effective discount.
++Incompatible reward-objective identities reset persisted HPO studies and cause
++stale best-parameter payloads to be ignored rather than warm-started.
++
+ ## 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
-| 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. |
++data prevents continuation but does not prevent inference. The deployment
++generation covers frozen feature coordinates and PBRS reward semantics. Otherwise
++compatible older generations, including `chronological-frozen-pipelines-v2`, support
++inference but cannot continue training. Reset trained models or use a new
++`freqai.identifier` to migrate. 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. |
-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
++| freqai.model_training_parameters.gamma | 0.95 | Finite Python/NumPy integer or floating scalar in [0,1]; null, bool, string, complex and arrays rejected | Omitted | Effective value is validated after HPO/resumed-learner precedence and propagated to environment potential_gamma. Invalid persisted gamma blocks continuation. |
+ | 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 sets terminal shaping to `-prev_potential`, even for tiny
++nonzero potentials. `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.
++
++Every enabled PBRS transition, including neutral steps with retained potential,
++uses `gamma * next_potential - prev_potential`. Both `canonical` and
++`non_canonical` have zero exit potential; the latter permits optional additives.
++The `pbrs_invariant` diagnostic recognizes either zero-exit mode when both
++effective additives are disabled. It is a conservative configuration check, not
++a guarantee about fitted policies: theoretical invariance also requires an
++observable state potential, the learner's discount, and appropriate boundaries.
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.
++trajectory is not a proof of universal policy invariance. Neither retained-exit
++modes nor additive-enabled configurations qualify merely because correction is
++zero; both zero-exit modes without effective additives remain eligible.
### 2. Parameter Sensitivity
- # 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
++- The owning test uses DECIMAL_PLACES_STRICT for zero next potential 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
++Do not generalize the zero voluntary-exit rule to every exit mode. For a voluntary
++exit with stored potential 2, gamma=0.95 and decay=0.5, the 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.
++A voluntary exit is not automatically a terminal transition. Actual environment
++termination clears next potential in every exit mode and sets shaping to
++`-prev_potential`. Name the applicable transition/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
++to be zero and the applicable additive terms to be absent. Both zero-exit modes
++are eligible under those conditions. 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.
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. |
+
+| cli-effective-reward-boundaries-145 | cli | Effective profit-target fallback and explicit zero idle denominator reach artifacts | cli/test_cli_params_and_csv.py::test_zero_target_and_idle_cap_reach_rewards_and_manifest | Metadata and near-target market coverage agree with runtime boundaries |
- | pbrs-native-integer-durations-146 | pbrs | Native integer durations and adjustment metadata retain exact values | pbrs/test_pbrs.py::test_native_integer_durations_preserve_exact_values | Python, signed NumPy and unsigned NumPy integers; strict and relaxed |
- | pbrs-native-duration-bounds-147 | pbrs | Floating duration bounds are checked before truncation without narrowing | pbrs/test_pbrs.py::test_native_duration_bounds_precede_truncation | Extended-precision cases run when supported by the platform |
++| pbrs-native-integer-durations-150 | pbrs | Native integer durations and adjustment metadata retain exact values | pbrs/test_pbrs.py::test_native_integer_durations_preserve_exact_values | Python, signed NumPy and unsigned NumPy integers; strict and relaxed |
++| pbrs-native-duration-bounds-151 | pbrs | Floating duration bounds are checked before truncation without narrowing | pbrs/test_pbrs.py::test_native_duration_bounds_precede_truncation | Extended-precision cases run when supported by the platform |
+| pbrs-native-gamma-reward-148 | pbrs | Finite out-of-range native gamma clamps reach the public reward | pbrs/test_pbrs.py::test_native_finite_gamma_clamps_the_public_reward | Extended-range cases run when supported by the platform |
+| pbrs-relaxed-native-gamma-149 | pbrs | Relaxed native gamma clamps reach rewards while strict validation rejects | pbrs/test_pbrs.py::test_relaxed_native_gamma_clamps_before_reward_calculation | Preserves direct, strict and relaxed boundary contracts |
+
### Non-Owning Smoke / Reference Checks
Tests that check an invariant owned elsewhere identify its owner in a comment
## 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.
+Real runtime/reference parity is owned by
+`ReforceXY/tests/test_model_pbrs_transitions.py::PbrsTransitionsTest::test_analysis_matches_real_rewards_across_modes_and_parameter_boundaries`.
+Native-precision rejection of out-of-range analytical discounts is owned by
+`ReforceXY/tests/test_model_pbrs_transitions.py::PbrsTransitionsTest::test_analysis_rejects_gamma_outside_bounds_before_float_rounding`.
+Run these tests in the Freqtrade RL QA image through the canonical runtime suite; the
+standalone analysis environment deliberately does not require Freqtrade/SB3.
+The comparator exercises direct/strict/relaxed parameter paths, real fills and
+terminal liquidation, carries reference potential independently, and checks tiny
+shaping components more tightly than the report-verification tolerance.
+Raw random shaping sums are not discounted invariance oracles. Complete-episode
+discounted identities and report continuity checks retain their existing owners.
+
## When to Run Tests
Run after changes to: reward component logic, PBRS mechanics, CLI
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)
--- /dev/null
-import unittest
+ """Config-template parser regressions; requires the matching Freqtrade QA image."""
+
-class ConfigTemplateTest(unittest.TestCase):
+ from pathlib import Path
+
++from qa_support import QaTestCase
++
+ from scripts.config_template_contract import (
+ assert_optional_ccxt_rate_limits_load_without_delimiter_edits,
+ )
+
+
++class ConfigTemplateTest(QaTestCase):
+ 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)