]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
fix(reforcexy): enforce analysis provenance contracts (#269)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Sun, 20 Sep 2026 11:19:06 +0000 (13:19 +0200)
committerGitHub <noreply@github.com>
Sun, 20 Sep 2026 11:19:06 +0000 (13:19 +0200)
* fix(reforcexy): enforce analysis provenance contracts

* refactor(reforcexy): harmonize manifest parameter domains

.github/workflows/quality.yml
README.md
ReforceXY/.basedpyright/diagnostics.json
ReforceXY/reward_space_analysis/README.md
ReforceXY/reward_space_analysis/reward_space_analysis.py
ReforceXY/reward_space_analysis/tests/README.md
ReforceXY/reward_space_analysis/tests/cli/test_cli_params_and_csv.py
ReforceXY/user_data/config-template.json

index e917c6225d2de9bd748d424d3fce9ab72153cdb9..66625cb9c658a73803f7cadd14e7483ef7ad97ff 100644 (file)
@@ -63,3 +63,34 @@ jobs:
             "${{ matrix.image }}" \
             /workspace/scripts/check_basedpyright.py \
             --project "${{ matrix.project }}"
+
+  reforcexy-tests:
+    name: ReforceXY tests
+    runs-on: ubuntu-latest
+    timeout-minutes: 30
+    env:
+      REFORCEXY_QA_IMAGE: freqai-strategies-reforcexy-qa:ci
+    steps:
+      - name: Check out repository
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+      - name: Build QA image
+        run: >-
+          docker build --pull --target qa
+          --tag "${REFORCEXY_QA_IMAGE}"
+          ReforceXY
+      - name: Run runtime regressions
+        run: |
+          docker run --rm \
+            --mount "type=bind,src=${GITHUB_WORKSPACE},dst=/workspace,readonly" \
+            --workdir /workspace \
+            --entrypoint python \
+            "${REFORCEXY_QA_IMAGE}" \
+            -m unittest discover -s ReforceXY/tests -v
+      - name: Set up uv
+        uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
+        with:
+          enable-cache: true
+          cache-dependency-glob: ReforceXY/reward_space_analysis/uv.lock
+      - name: Run analysis regressions
+        working-directory: ReforceXY/reward_space_analysis
+        run: uv run --locked --extra dev pytest
index c6d308bea09503db7247b5513d7a02612f3932e4..ab08b9f6ba517eb86a8e4eef845dfad966ddbe35 100644 (file)
--- a/README.md
+++ b/README.md
@@ -62,16 +62,16 @@ below.
 | _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.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
+| 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. |
+| 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_                                        |                          |                                                                                                                                                                                                              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
@@ -95,7 +95,7 @@ below.
 | 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_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"`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
@@ -122,7 +122,7 @@ below.
 | 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_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`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
@@ -136,7 +136,7 @@ below.
 | 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.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.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
@@ -224,7 +224,7 @@ does not accept zero. Use RMSE to diagnose prediction quality, not profitability
    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
+   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
@@ -340,6 +340,7 @@ docker compose up -d --build
 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.
 
index c318288aa1aa7656a1986b1d7add817b95cc7927..509c3a81641d0ca054897ddbf5de5672764a614f 100644 (file)
     },
     {
       "endCharacter": 5,
-      "endLine": 2047,
+      "endLine": 2048,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "No overloads for \"cut\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 17,
-      "startLine": 2042
+      "startLine": 2043
     },
     {
       "endCharacter": 21,
-      "endLine": 2044,
+      "endLine": 2045,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Argument of type \"_Array1D[Any]\" cannot be assigned to parameter \"bins\" of type \"int | Sequence[float] | Index[int] | Index[float] | IntervalIndex[Interval[Any]] | Series[Any]\" in function \"cut\"\n  Type \"_Array1D[Any]\" is not assignable to type \"int | Sequence[float] | Index[int] | Index[float] | IntervalIndex[Interval[Any]] | Series[Any]\"\n    \"ndarray[tuple[int], dtype[Any]]\" is not assignable to \"int\"\n    \"ndarray[tuple[int], dtype[Any]]\" is not assignable to \"Sequence[float]\"\n    \"ndarray[tuple[int], dtype[Any]]\" is not assignable to \"Index[int]\"\n    \"ndarray[tuple[int], dtype[Any]]\" is not assignable to \"Index[float]\"\n    \"ndarray[tuple[int], dtype[Any]]\" is not assignable to \"IntervalIndex[Interval[Any]]\"\n    \"ndarray[tuple[int], dtype[Any]]\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 13,
-      "startLine": 2044
+      "startLine": 2045
     },
     {
       "endCharacter": 5,
-      "endLine": 2069,
+      "endLine": 2070,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Type \"dict[bytes, bytes] | dict[str, RewardParamValue]\" is not assignable to declared type \"RewardParams\"\n  Type \"dict[bytes, bytes] | dict[str, RewardParamValue]\" is not assignable to type \"RewardParams\"\n    \"dict[bytes, bytes]\" is not assignable to \"dict[str, RewardParamValue]\"\n      Type parameter \"_KT@dict\" is invariant, but \"bytes\" is not the same as \"str\"\n      Type parameter \"_VT@dict\" is invariant, but \"bytes\" is not the same as \"RewardParamValue\"",
       "rule": "reportAssignmentType",
       "severity": "error",
       "startCharacter": 34,
-      "startLine": 2065
+      "startLine": 2066
     },
     {
       "endCharacter": 43,
-      "endLine": 2066,
+      "endLine": 2067,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "No overloads for \"__init__\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 8,
-      "startLine": 2066
+      "startLine": 2067
     },
     {
       "endCharacter": 42,
-      "endLine": 2066,
+      "endLine": 2067,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"iterable\" of type \"Iterable[list[bytes]]\" in function \"__init__\"\n  Type \"Any | None\" is not assignable to type \"Iterable[list[bytes]]\"\n    \"None\" is incompatible with protocol \"Iterable[list[bytes]]\"\n      \"__iter__\" is not present",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 13,
-      "startLine": 2066
+      "startLine": 2067
     },
     {
       "endCharacter": 60,
-      "endLine": 2326,
+      "endLine": 2327,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Cannot access attribute \"importances_mean\" for class \"dict[Unknown, Bunch]\"\n  Attribute \"importances_mean\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 44,
-      "startLine": 2326
+      "startLine": 2327
     },
     {
       "endCharacter": 58,
-      "endLine": 2327,
+      "endLine": 2328,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Cannot access attribute \"importances_std\" for class \"dict[Unknown, Bunch]\"\n  Attribute \"importances_std\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 43,
-      "startLine": 2327
+      "startLine": 2328
     },
     {
       "endCharacter": 88,
-      "endLine": 2346,
+      "endLine": 2347,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Cannot access attribute \"columns\" for class \"NDArray[Unknown]\"\n  Attribute \"columns\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 81,
-      "startLine": 2346
+      "startLine": 2347
     },
     {
       "endCharacter": 88,
-      "endLine": 2346,
+      "endLine": 2347,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Cannot access attribute \"columns\" for class \"list[Unknown]\"\n  Attribute \"columns\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 81,
-      "startLine": 2346
+      "startLine": 2347
     },
     {
       "endCharacter": 17,
-      "endLine": 2355,
+      "endLine": 2356,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Object of type \"None\" cannot be called",
       "rule": "reportOptionalCall",
       "severity": "error",
       "startCharacter": 28,
-      "startLine": 2349
+      "startLine": 2350
     },
     {
       "endCharacter": 40,
-      "endLine": 2567,
+      "endLine": 2575,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Argument of type \"Any | object | NAType | Unknown\" cannot be assigned to parameter \"arg1\" of type \"SupportsRichComparisonT@min\" in function \"min\"\n  Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n    Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n      Type \"object\" is not assignable to type \"SupportsRichComparison\"\n        \"object\" is incompatible with protocol \"SupportsDunderLT[Any]\"\n          \"__lt__\" is not present\n        \"object\" is incompatible with protocol \"SupportsDunderGT[Any]\"\n          \"__gt__\" is not present",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 22,
-      "startLine": 2567
+      "startLine": 2575
     },
     {
       "endCharacter": 38,
-      "endLine": 2567,
+      "endLine": 2575,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Cannot access attribute \"min\" for class \"ExtensionArray\"\n  Attribute \"min\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 35,
-      "startLine": 2567
+      "startLine": 2575
     },
     {
       "endCharacter": 59,
-      "endLine": 2567,
+      "endLine": 2575,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Argument of type \"Any | object | NAType | Unknown\" cannot be assigned to parameter \"arg2\" of type \"SupportsRichComparisonT@min\" in function \"min\"\n  Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n    Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n      Type \"object\" is not assignable to type \"SupportsRichComparison\"\n        \"object\" is incompatible with protocol \"SupportsDunderLT[Any]\"\n          \"__lt__\" is not present\n        \"object\" is incompatible with protocol \"SupportsDunderGT[Any]\"\n          \"__gt__\" is not present",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 42,
-      "startLine": 2567
+      "startLine": 2575
     },
     {
       "endCharacter": 57,
-      "endLine": 2567,
+      "endLine": 2575,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Cannot access attribute \"min\" for class \"ExtensionArray\"\n  Attribute \"min\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 54,
-      "startLine": 2567
+      "startLine": 2575
     },
     {
       "endCharacter": 40,
-      "endLine": 2568,
+      "endLine": 2576,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Argument of type \"Any | object | NAType | Unknown\" cannot be assigned to parameter \"arg1\" of type \"SupportsRichComparisonT@max\" in function \"max\"\n  Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n    Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n      Type \"object\" is not assignable to type \"SupportsRichComparison\"\n        \"object\" is incompatible with protocol \"SupportsDunderLT[Any]\"\n          \"__lt__\" is not present\n        \"object\" is incompatible with protocol \"SupportsDunderGT[Any]\"\n          \"__gt__\" is not present",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 22,
-      "startLine": 2568
+      "startLine": 2576
     },
     {
       "endCharacter": 38,
-      "endLine": 2568,
+      "endLine": 2576,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Cannot access attribute \"max\" for class \"ExtensionArray\"\n  Attribute \"max\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 35,
-      "startLine": 2568
+      "startLine": 2576
     },
     {
       "endCharacter": 59,
-      "endLine": 2568,
+      "endLine": 2576,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Argument of type \"Any | object | NAType | Unknown\" cannot be assigned to parameter \"arg2\" of type \"SupportsRichComparisonT@max\" in function \"max\"\n  Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n    Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n      Type \"object\" is not assignable to type \"SupportsRichComparison\"\n        \"object\" is incompatible with protocol \"SupportsDunderLT[Any]\"\n          \"__lt__\" is not present\n        \"object\" is incompatible with protocol \"SupportsDunderGT[Any]\"\n          \"__gt__\" is not present",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 42,
-      "startLine": 2568
+      "startLine": 2576
     },
     {
       "endCharacter": 57,
-      "endLine": 2568,
+      "endLine": 2576,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Cannot access attribute \"max\" for class \"ExtensionArray\"\n  Attribute \"max\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 54,
-      "startLine": 2568
+      "startLine": 2576
     },
     {
       "endCharacter": 76,
-      "endLine": 2584,
+      "endLine": 2592,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "No overloads for \"histogram\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 24,
-      "startLine": 2584
+      "startLine": 2592
     },
     {
       "endCharacter": 49,
-      "endLine": 2584,
+      "endLine": 2592,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"a\" of type \"_ArrayLikeComplex_co\" in function \"histogram\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"_ArrayLikeComplex_co\"\n    Type \"ExtensionArray\" is not assignable to type \"_ArrayLikeComplex_co\"\n      \"ExtensionArray\" is incompatible with protocol \"_SupportsArray[dtype[numpy.bool[builtins.bool] | number[Any, Any]]]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"_NestedSequence[_SupportsArray[dtype[numpy.bool[builtins.bool] | number[Any, Any]]]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n  ...",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 37,
-      "startLine": 2584
+      "startLine": 2592
     },
     {
       "endCharacter": 74,
-      "endLine": 2585,
+      "endLine": 2593,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "No overloads for \"histogram\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 23,
-      "startLine": 2585
+      "startLine": 2593
     },
     {
       "endCharacter": 47,
-      "endLine": 2585,
+      "endLine": 2593,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"a\" of type \"_ArrayLikeComplex_co\" in function \"histogram\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"_ArrayLikeComplex_co\"\n    Type \"ExtensionArray\" is not assignable to type \"_ArrayLikeComplex_co\"\n      \"ExtensionArray\" is incompatible with protocol \"_SupportsArray[dtype[numpy.bool[builtins.bool] | number[Any, Any]]]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"_NestedSequence[_SupportsArray[dtype[numpy.bool[builtins.bool] | number[Any, Any]]]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n  ...",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 36,
-      "startLine": 2585
+      "startLine": 2593
     },
     {
       "endCharacter": 51,
-      "endLine": 2600,
+      "endLine": 2608,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"u_values\" of type \"ToFloatND\" in function \"wasserstein_distance\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"ToFloatND\"\n    Type \"ExtensionArray\" is not assignable to type \"ToFloatND\"\n      \"ExtensionArray\" is incompatible with protocol \"_CanArrayND[floating_co]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"SequenceND[py_float | _CanArray[floating_co]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n  ...",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 39,
-      "startLine": 2600
+      "startLine": 2608
     },
     {
       "endCharacter": 64,
-      "endLine": 2600,
+      "endLine": 2608,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"v_values\" of type \"ToFloatND\" in function \"wasserstein_distance\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"ToFloatND\"\n    Type \"ExtensionArray\" is not assignable to type \"ToFloatND\"\n      \"ExtensionArray\" is incompatible with protocol \"_CanArrayND[floating_co]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"SequenceND[py_float | _CanArray[floating_co]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n  ...",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 53,
-      "startLine": 2600
+      "startLine": 2608
     },
     {
       "endCharacter": 68,
-      "endLine": 2603,
+      "endLine": 2611,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "No overloads for \"ks_2samp\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 27,
-      "startLine": 2603
+      "startLine": 2611
     },
     {
       "endCharacter": 54,
-      "endLine": 2603,
+      "endLine": 2611,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"data1\" of type \"ToFloatND\" in function \"ks_2samp\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"ToFloatND\"\n    Type \"ExtensionArray\" is not assignable to type \"ToFloatND\"\n      \"ExtensionArray\" is incompatible with protocol \"_CanArrayND[floating_co]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"SequenceND[py_float | _CanArray[floating_co]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n  ...",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 42,
-      "startLine": 2603
+      "startLine": 2611
     },
     {
       "endCharacter": 67,
-      "endLine": 2603,
+      "endLine": 2611,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"data2\" of type \"ToFloatND\" in function \"ks_2samp\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"ToFloatND\"\n    Type \"ExtensionArray\" is not assignable to type \"ToFloatND\"\n      \"ExtensionArray\" is incompatible with protocol \"_CanArrayND[floating_co]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"SequenceND[py_float | _CanArray[floating_co]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n  ...",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 56,
-      "startLine": 2603
+      "startLine": 2611
     },
     {
       "endCharacter": 29,
-      "endLine": 2903,
+      "endLine": 2911,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "No overloads for \"ptp\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 11,
-      "startLine": 2903
+      "startLine": 2911
     },
     {
       "endCharacter": 28,
-      "endLine": 2903,
+      "endLine": 2911,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"a\" of type \"_ArrayLikeNumeric_co\" in function \"ptp\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"_ArrayLikeNumeric_co\"\n    Type \"ExtensionArray\" is not assignable to type \"_ArrayLikeNumeric_co\"\n      \"ExtensionArray\" is incompatible with protocol \"_SupportsArray[dtype[number[Any, Any] | numpy.bool[builtins.bool] | object_ | timedelta64[Any]]]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"_NestedSequence[_SupportsArray[dtype[number[Any, Any] | numpy.bool[builtins.bool] | object_ | timedelta64[Any]]]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n  ...",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 18,
-      "startLine": 2903
+      "startLine": 2911
     },
     {
       "endCharacter": 65,
-      "endLine": 2912,
+      "endLine": 2920,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Cannot access attribute \"mean\" for class \"Categorical[object]\"\n  Attribute \"mean\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 61,
-      "startLine": 2912
+      "startLine": 2920
     },
     {
       "endCharacter": 65,
-      "endLine": 2912,
+      "endLine": 2920,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Cannot access attribute \"mean\" for class \"ExtensionArray\"\n  Attribute \"mean\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 61,
-      "startLine": 2912
+      "startLine": 2920
     },
     {
       "endCharacter": 5,
-      "endLine": 3803,
+      "endLine": 3811,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Type \"dict[bytes, bytes] | dict[str, RewardParamValue]\" is not assignable to declared type \"RewardParams\"\n  Type \"dict[bytes, bytes] | dict[str, RewardParamValue]\" is not assignable to type \"RewardParams\"\n    \"dict[bytes, bytes]\" is not assignable to \"dict[str, RewardParamValue]\"\n      Type parameter \"_KT@dict\" is invariant, but \"bytes\" is not the same as \"str\"\n      Type parameter \"_VT@dict\" is invariant, but \"bytes\" is not the same as \"RewardParamValue\"",
       "rule": "reportAssignmentType",
       "severity": "error",
       "startCharacter": 34,
-      "startLine": 3799
+      "startLine": 3807
     },
     {
       "endCharacter": 43,
-      "endLine": 3800,
+      "endLine": 3808,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "No overloads for \"__init__\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 8,
-      "startLine": 3800
+      "startLine": 3808
     },
     {
       "endCharacter": 42,
-      "endLine": 3800,
+      "endLine": 3808,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"iterable\" of type \"Iterable[list[bytes]]\" in function \"__init__\"\n  Type \"Any | None\" is not assignable to type \"Iterable[list[bytes]]\"\n    \"None\" is incompatible with protocol \"Iterable[list[bytes]]\"\n      \"__iter__\" is not present",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 13,
-      "startLine": 3800
-    },
-    {
-      "endCharacter": 14,
-      "endLine": 4647,
-      "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
-      "message": "Declaration \"sim_params\" is obscured by a declaration of the same name",
-      "rule": "reportRedeclaration",
-      "severity": "error",
-      "startCharacter": 4,
-      "startLine": 4647
+      "startLine": 3808
     },
     {
       "endCharacter": 22,
index 8c342106f5e91095fafb1468c4d1af876b51005c..f5f57b5b386dd2cee55e7355abdf99d965f67d01 100644 (file)
@@ -19,19 +19,19 @@ PBRS invariance.
 ```shell
 # Install
 cd ReforceXY/reward_space_analysis
-uv sync --all-groups
+uv sync --locked --extra dev
 
 # Run a default analysis
 uv run python reward_space_analysis.py --num_samples 20000 --out_dir out
 
 # Run test suite (coverage ≥85% enforced)
-uv run pytest
+uv run --locked --extra dev pytest
 ```
 
 Minimal selective test example:
 
 ```shell
-uv run pytest -m pbrs -q
+uv run --locked --extra dev pytest -m pbrs -q
 ```
 
 Full test documentation: [tests/README.md](./tests/README.md).
@@ -88,7 +88,7 @@ Setup with uv:
 
 ```shell
 cd ReforceXY/reward_space_analysis
-uv sync --all-groups
+uv sync --locked --extra dev
 ```
 
 Run:
@@ -307,14 +307,14 @@ where `kernel_function` depends on `exit_attenuation_mode`. See
 
 #### Duration Penalties
 
-| Parameter                    | Default | Description                |
-| ---------------------------- | ------- | -------------------------- |
-| `max_trade_duration_candles` | 128     | Trade duration cap         |
+| Parameter                    | Default | Description                                                                               |
+| ---------------------------- | ------- | ----------------------------------------------------------------------------------------- |
+| `max_trade_duration_candles` | 128     | Trade duration cap                                                                        |
 | `max_idle_duration_candles`  | None    | Idle hazard threshold (4× trade duration fallback); the idle clock keeps counting past it |
-| `idle_penalty_ratio`         | 1.0     | Idle penalty ratio         |
-| `idle_penalty_power`         | 1.025   | Idle penalty exponent      |
-| `hold_penalty_ratio`         | 1.0     | Hold penalty ratio         |
-| `hold_penalty_power`         | 1.025   | Hold penalty exponent      |
+| `idle_penalty_ratio`         | 1.0     | Idle penalty ratio                                                                        |
+| `idle_penalty_power`         | 1.025   | Idle penalty exponent                                                                     |
+| `hold_penalty_ratio`         | 1.0     | Hold penalty ratio                                                                        |
+| `hold_penalty_power`         | 1.025   | Hold penalty exponent                                                                     |
 
 #### Validation
 
@@ -413,23 +413,23 @@ r* = r - grace    if exit_plateau and r > grace
 r* = r            if not exit_plateau
 ```
 
-| Mode      | Formula                       | Monotonic | Notes                                       | Use Case                             |
-| --------- | ----------------------------- | --------- | ------------------------------------------- | ------------------------------------ |
-| sqrt      | 1 / √(1 + r\*)                | Yes       | Sub-linear decay                            | Gentle long-trade penalty            |
-| linear    | 1 / (1 + slope · r\*)         | Yes       | slope = `exit_linear_slope`                 | Balanced duration penalty (default)  |
-| power     | (1 + r\*)^(-alpha)            | Yes       | alpha = -ln(tau)/ln(2); tau=1 ⇒ alpha=0     | Tunable decay rate via tau parameter |
-| half_life | 2^(-r\* / hl)                 | Yes       | hl = `exit_half_life`; r\*=hl ⇒ factor 0.5  | Time-based exponential discount      |
+| Mode      | Formula               | Monotonic | Notes                                      | Use Case                             |
+| --------- | --------------------- | --------- | ------------------------------------------ | ------------------------------------ |
+| sqrt      | 1 / √(1 + r\*)        | Yes       | Sub-linear decay                           | Gentle long-trade penalty            |
+| linear    | 1 / (1 + slope · r\*) | Yes       | slope = `exit_linear_slope`                | Balanced duration penalty (default)  |
+| power     | (1 + r\*)^(-alpha)    | Yes       | alpha = -ln(tau)/ln(2); tau=1 ⇒ alpha=0    | Tunable decay rate via tau parameter |
+| half_life | 2^(-r\* / hl)         | Yes       | hl = `exit_half_life`; r\*=hl ⇒ factor 0.5 | Time-based exponential discount      |
 
 ### Transform Functions
 
-| Transform  | Formula                          | Range   | Characteristics   | Use Case                      |
-| ---------- | -------------------------------- | ------- | ----------------- | ----------------------------- |
-| `tanh`     | tanh(x)                          | (-1, 1) | Smooth sigmoid    | Balanced transforms (default) |
-| `softsign` | x / (1 + \|x\|)                  | (-1, 1) | Linear near 0     | Less aggressive saturation    |
-| `arctan`   | (2/π) · arctan(x)                | (-1, 1) | Slower saturation | Wide dynamic range            |
-| `sigmoid`  | 2σ(x) - 1, σ(x) = 1/(1 + e^(-x)) | (-1, 1) | Standard sigmoid  | Generic shaping               |
-| `softsign_sqrt` | x / √(1 + x²)             | (-1, 1) | Outlier robust    | Extreme stability             |
-| `clip`     | clip(x, -1, 1)                   | [-1, 1] | Hard clipping     | Preserve linearity            |
+| Transform       | Formula                          | Range   | Characteristics   | Use Case                      |
+| --------------- | -------------------------------- | ------- | ----------------- | ----------------------------- |
+| `tanh`          | tanh(x)                          | (-1, 1) | Smooth sigmoid    | Balanced transforms (default) |
+| `softsign`      | x / (1 + \|x\|)                  | (-1, 1) | Linear near 0     | Less aggressive saturation    |
+| `arctan`        | (2/π) · arctan(x)                | (-1, 1) | Slower saturation | Wide dynamic range            |
+| `sigmoid`       | 2σ(x) - 1, σ(x) = 1/(1 + e^(-x)) | (-1, 1) | Standard sigmoid  | Generic shaping               |
+| `softsign_sqrt` | x / √(1 + x²)                    | (-1, 1) | Outlier robust    | Extreme stability             |
+| `clip`          | clip(x, -1, 1)                   | [-1, 1] | Hard clipping     | Preserve linearity            |
 
 ### Skipping Feature Analysis
 
@@ -536,21 +536,24 @@ descriptive.
 | `num_samples`           | int               | Synthetic samples count           |
 | `seed`                  | int               | Master random seed                |
 | `pnl_target`            | float             | Profit target                     |
-| `parameter_adjustments` | object            | Bound clamp adjustments (if any)  |
-| `reward_params`         | object            | Final reward params               |
-| `simulation_params`     | object            | All simulation inputs             |
-| `params_hash`           | string (sha256)   | Deterministic run hash            |
+| `parameter_adjustments` | object            | Validation coercions, bound clamps, and derived-default adjustments |
+| `reward_params`         | object            | Resolved reward tunables, excluding the separate effective scalars |
+| `effective`             | object            | Resolved base factor, profit aim, and risk/reward ratio           |
+| `simulation_params`     | object            | Resolved simulation/report controls, including action masking, unrealized PnL, and real-data provenance |
+| `params_hash`           | string (sha256)   | Hash of effective inputs, excluding output and real-data paths    |
 
-Two runs match iff `params_hash` identical.
+The `reward_params`, `effective`, and `simulation_params` maps have disjoint keys. Simulation controls are derived from the parsed CLI options; the invocation-only `params` and `strict_validation` options are excluded. Their resolved effects are recorded in the parameter maps and validation adjustments.
+
+Within the same analyzer revision, identical `params_hash` values mean the resolved configuration and real-episode bytes match. Equivalent flag and `--params` inputs therefore share one hash.
 
 ### Distribution Shift Metrics
 
-| Metric            | Definition                            | Notes                         |
-| ----------------- | ------------------------------------- | ----------------------------- |
-| `*_kl_divergence` | KL(synth‖real) = Σ p_s log(p_s / p_r) | 0 ⇒ identical histograms      |
-| `*_js_distance`   | √(0.5 KL(p_s‖m) + 0.5 KL(p_r‖m))      | Symmetric, [0,1]              |
-| `*_wasserstein`   | 1D Earth Mover's Distance             | Units of feature              |
-| `*_ks_statistic`  | KS two-sample statistic               | [0,1]; higher ⇒ divergence    |
+| Metric            | Definition                            | Notes                                                                         |
+| ----------------- | ------------------------------------- | ----------------------------------------------------------------------------- |
+| `*_kl_divergence` | KL(synth‖real) = Σ p_s log(p_s / p_r) | 0 ⇒ identical histograms                                                      |
+| `*_js_distance`   | √(0.5 KL(p_s‖m) + 0.5 KL(p_r‖m))      | Symmetric, [0,1]                                                              |
+| `*_wasserstein`   | 1D Earth Mover's Distance             | Units of feature                                                              |
+| `*_ks_statistic`  | KS two-sample statistic               | [0,1]; higher ⇒ divergence                                                    |
 | `*_ks_pvalue`     | KS test p-value                       | API-only with `independent_observations=True`; omitted by the descriptive CLI |
 
 Implementation: 50-bin histograms with ε=1e-10; constants have zero divergence.
@@ -620,13 +623,13 @@ EOF
 Quick validation:
 
 ```shell
-uv run pytest
+uv run --locked --extra dev pytest
 ```
 
 Selective example:
 
 ```shell
-uv run pytest -m pbrs -q
+uv run --locked --extra dev pytest -m pbrs -q
 ```
 
 Coverage threshold enforced: 85% (`--cov-fail-under=85` in `pyproject.toml`).
index 5c8c69049a486243f2fc0e2268473b1a282dab17..f75b1c1cb3c7ff8f27f06ae22f6883c7e99c4575 100644 (file)
@@ -576,27 +576,25 @@ def validate_reward_parameters(
     adjustments: dict[str, dict[str, Any]] = {}
 
     # Boolean parameter coercion
-    _bool_keys = [
+    bool_keys = (
         "check_invariants",
+        "exit_plateau",
         "hold_potential_enabled",
         "entry_additive_enabled",
         "exit_additive_enabled",
-    ]
-    for bkey in _bool_keys:
-        if bkey in sanitized:
-            original_val = sanitized[bkey]
-            coerced_val = _to_bool(original_val)
-            if coerced_val is not original_val:
-                sanitized[bkey] = coerced_val
-            adjustments.setdefault(
-                bkey,
-                {
-                    "original": original_val,
-                    "adjusted": coerced_val,
+    )
+    for key in bool_keys:
+        if key in sanitized:
+            original_value = sanitized[key]
+            coerced_value = _to_bool(original_value)
+            if coerced_value is not original_value:
+                sanitized[key] = coerced_value
+                adjustments[key] = {
+                    "original": original_value,
+                    "adjusted": coerced_value,
                     "reason": "bool_coerce",
                     "validation_mode": "strict" if strict else "relaxed",
-                },
-            )
+                }
 
     # Coerce and clamp numeric-bounded parameters
     for key, bounds in _PARAMETER_BOUNDS.items():
@@ -698,6 +696,9 @@ def validate_reward_parameters(
                 "reason": "negative_efficiency_guard",
                 "validation_mode": "relaxed",
             }
+    for key in ("max_trade_duration_candles", "max_idle_duration_candles"):
+        if key in sanitized:
+            sanitized[key] = int(_get_float_param(sanitized, key))
 
     return sanitized, adjustments
 
@@ -2384,15 +2385,22 @@ def _perform_feature_analysis(
     return importance_df, analysis_stats, partial_deps, model
 
 
-def load_real_episodes(path: Path, *, enforce_columns: bool = True) -> pd.DataFrame:
+def load_real_episodes(
+    path: Path,
+    *,
+    enforce_columns: bool = True,
+    artifact_bytes: bytes | None = None,
+) -> pd.DataFrame:
     """Load serialized episodes into normalized DataFrame.
 
     Parameters
     ----------
     path : Path
-        Pickle file path.
+        Pickle file path used for diagnostics.
     enforce_columns : bool, default True
         Require all expected columns (raise on missing) or fill with NaN.
+    artifact_bytes : bytes, optional
+        Exact artifact bytes to deserialize instead of reading the path.
 
     Returns
     -------
@@ -2401,10 +2409,10 @@ def load_real_episodes(path: Path, *, enforce_columns: bool = True) -> pd.DataFr
     """
 
     try:
-        with path.open("rb") as f:
-            episodes_data = pickle.load(f)
-    except Exception as e:
-        raise ValueError(f"Data: failed to unpickle '{path}': {e!r}") from e
+        serialized = path.read_bytes() if artifact_bytes is None else artifact_bytes
+        episodes_data = pickle.loads(serialized)
+    except Exception as exc:
+        raise ValueError(f"Data: failed to unpickle '{path}': {exc!r}") from exc
 
     # Top-level dict with 'transitions'
     if isinstance(episodes_data, dict) and "transitions" in episodes_data:
@@ -4572,16 +4580,25 @@ def main() -> None:
     base_factor = _get_float_param(params, "base_factor", float(args.base_factor))
     profit_aim = _get_float_param(params, "profit_aim", float(args.profit_aim))
     risk_reward_ratio = _get_float_param(params, "risk_reward_ratio", float(args.risk_reward_ratio))
+    effective_params = {
+        "base_factor": base_factor,
+        "profit_aim": profit_aim,
+        "risk_reward_ratio": risk_reward_ratio,
+    }
+    params["max_idle_duration_candles"] = get_max_idle_duration_candles(params)
     params["action_masking"] = _to_bool(params.get("action_masking", args.action_masking))
     params["unrealized_pnl"] = bool(args.unrealized_pnl)
     # Deterministic seeds cascade
     random.seed(args.seed)
     np.random.seed(args.seed)
     real_df = None
+    real_episodes_sha256 = None
     if args.real_episodes is not None:
         # Fail before any artifact is written for an explicitly requested file.
         print(f"CLI: Loading real episodes from {args.real_episodes}")
-        real_df = load_real_episodes(args.real_episodes)
+        real_episode_bytes = args.real_episodes.read_bytes()
+        real_df = load_real_episodes(args.real_episodes, artifact_bytes=real_episode_bytes)
+        real_episodes_sha256 = hashlib.sha256(real_episode_bytes).hexdigest()
 
     df = simulate_samples(
         num_samples=args.num_samples,
@@ -4616,49 +4633,17 @@ def main() -> None:
             "Sim: NaN values detected in critical simulated columns: "
             + ", ".join(f"{k}={v}" for k, v in nan_issues.items())
         )
-    # Attach simulation parameters for downstream manifest
-    try:
-        defaults = {
-            a.dest: getattr(a, "default", None) for a in parser._actions if hasattr(a, "dest")
-        }
-    except Exception:
-        defaults = {}
-    args_dict = vars(args)
-
-    candidate_keys = [
-        "num_samples",
-        "seed",
-        "out_dir",
-        "trading_mode",
-        "risk_reward_ratio",
-        "profit_aim",
-        "max_duration_ratio",
-        "pnl_base_std",
-        "pnl_duration_vol_scale",
-        "rf_n_jobs",
-        "perm_n_jobs",
-        "skip_feature_analysis",
-        "skip_partial_dependence",
-        "strict_diagnostics",
-        "real_episodes",
-        "unrealized_pnl",
-        "action_masking",
-    ]
-
-    sim_params: dict[str, Any] = {}
-    for k in candidate_keys:
-        if k in args_dict:
-            v = args_dict[k]
-            v_norm = str(v) if isinstance(v, Path) else v
-            d = defaults.get(k)
-            d_norm = str(d) if isinstance(d, Path) else d
-            if d_norm != v_norm:
-                sim_params[k] = v_norm
-
-    # Deduplicate any keys that overlap with reward_params (single source of truth)
-    for k in list(sim_params.keys()):
-        if k in params:
-            sim_params.pop(k)
+    # Derive simulation controls from parsed options, not a parallel option inventory.
+    sim_params: dict[str, Any] = {
+        key: str(value) if isinstance(value, Path) else value
+        for key, value in vars(args).items()
+        if key not in DEFAULT_MODEL_REWARD_PARAMETERS
+        and key not in effective_params
+        and key not in {"params", "strict_validation"}
+    }
+    sim_params["action_masking"] = params["action_masking"]
+    sim_params["unrealized_pnl"] = params["unrealized_pnl"]
+    sim_params["real_episodes_sha256"] = real_episodes_sha256
 
     df.attrs["simulation_params"] = sim_params
     df.attrs["reward_params"] = dict(params)
@@ -4688,9 +4673,11 @@ def main() -> None:
     # Generate manifest summarizing key metrics
     try:
         manifest_path = args.out_dir / "manifest.json"
-        resolved_reward_params: dict[str, Any] = dict(
-            params
-        )  # already validated/normalized upstream
+        resolved_reward_params: dict[str, Any] = {
+            key: value
+            for key, value in params.items()
+            if key not in effective_params and key not in sim_params
+        }
         manifest: dict[str, Any] = {
             "generated_at": pd.Timestamp.now().isoformat(),
             "num_samples": len(df),
@@ -4698,36 +4685,28 @@ def main() -> None:
             "pnl_target": float(profit_aim * risk_reward_ratio),
             "parameter_adjustments": adjustments,
             "reward_params": resolved_reward_params,
-            "effective": {
-                "base_factor": float(base_factor),
-                "profit_aim": float(profit_aim),
-                "risk_reward_ratio": float(risk_reward_ratio),
+            "effective": effective_params,
+        }
+        excluded_for_hash = {"out_dir", "real_episodes"}
+        sim_params_for_hash: dict[str, Any] = {
+            key: sim_params[key] for key in sim_params if key not in excluded_for_hash
+        }
+        hash_source: dict[str, Any] = {
+            **{f"sim::{key}": sim_params_for_hash[key] for key in sorted(sim_params_for_hash)},
+            **{
+                f"reward::{key}": resolved_reward_params[key]
+                for key in sorted(resolved_reward_params)
             },
+            **{f"effective::{key}": effective_params[key] for key in sorted(effective_params)},
         }
-        sim_params_dict = df.attrs.get("simulation_params", {})
-        if not isinstance(sim_params_dict, dict):
-            sim_params_dict = {}
-        sim_params: dict[str, Any] = dict(sim_params_dict)
-        if sim_params:
-            excluded_for_hash = {"out_dir", "real_episodes"}
-            sim_params_for_hash: dict[str, Any] = {
-                k: sim_params[k] for k in sim_params if k not in excluded_for_hash
-            }
-            _hash_source: dict[str, Any] = {
-                **{f"sim::{k}": sim_params_for_hash[k] for k in sorted(sim_params_for_hash)},
-                **{
-                    f"reward::{k}": resolved_reward_params[k]
-                    for k in sorted(resolved_reward_params)
-                },
-            }
-            _hash_source_str = json.dumps(_hash_source, sort_keys=True)
-            manifest["params_hash"] = hashlib.sha256(_hash_source_str.encode("utf-8")).hexdigest()
-            manifest["simulation_params"] = sim_params
+        hash_source_json = json.dumps(hash_source, sort_keys=True)
+        manifest["params_hash"] = hashlib.sha256(hash_source_json.encode("utf-8")).hexdigest()
+        manifest["simulation_params"] = sim_params
         with manifest_path.open("w", encoding="utf-8") as mh:
             json.dump(manifest, mh, indent=2)
         print(f"CLI: Manifest saved to {manifest_path}")
-    except Exception as e:
-        print(f"CLI: Manifest generation failed; {e}")
+    except Exception as exc:
+        raise RuntimeError(f"CLI: Manifest generation failed: {exc}") from exc
 
     print(f"CLI: Generated {len(df):,} synthetic samples")
     print(f"CLI: {sample_output_message}")
index bc6368611edec319986346553fe9b43fec362786..a5172e84269edb38c1c166ceb3d240293cf9a1f5 100644 (file)
@@ -143,29 +143,29 @@ def test_quick_check(self): ...
 Full suite (coverage ≥85% enforced):
 
 ```shell
-uv run pytest
+uv run --locked --extra dev pytest
 ```
 
 Selective markers:
 
 ```shell
-uv run pytest -m pbrs -q
-uv run pytest -m robustness -q
-uv run pytest -m "components or robustness" -q
-uv run pytest -m "not slow" -q
+uv run --locked --extra dev pytest -m pbrs -q
+uv run --locked --extra dev pytest -m robustness -q
+uv run --locked --extra dev pytest -m "components or robustness" -q
+uv run --locked --extra dev pytest -m "not slow" -q
 ```
 
 Coverage reports:
 
 ```shell
-uv run pytest --cov=reward_space_analysis --cov-report=term-missing
-uv run pytest --cov=reward_space_analysis --cov-report=html && open htmlcov/index.html
+uv run --locked --extra dev pytest --cov=reward_space_analysis --cov-report=term-missing
+uv run --locked --extra dev pytest --cov=reward_space_analysis --cov-report=html && open htmlcov/index.html
 ```
 
 Slow statistical tests:
 
 ```shell
-uv run pytest -m "statistics and slow" -q
+uv run --locked --extra dev pytest -m "statistics and slow" -q
 ```
 
 ## Coverage Mapping (Invariant Ownership)
@@ -180,34 +180,34 @@ Columns:
 - Notes: Clarifications (sub-modes, extensions, non-owning references elsewhere,
   line clusters for multi-path coverage).
 
-| ID                                           | Category    | Description                                                                         | Owning File                               | Notes                                                                                                        |
-| -------------------------------------------- | ----------- | ----------------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
-| report-additives-deterministic-092           | components  | Additives deterministic report section                                              | components/test_additives.py:4            | 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:43          | 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:654         | Comment line (function at :655)                                                                              |
-| robustness-negative-grace-clamp-103          | robustness  | Negative exit_plateau_grace clamps to 0.0 w/ warning                                | robustness/test_robustness.py:696         |                                                                                                              |
-| robustness-invalid-power-tau-104             | robustness  | Invalid power tau falls back alpha=1.0 w/ warning                                   | robustness/test_robustness.py:747         |                                                                                                              |
-| robustness-near-zero-half-life-105           | robustness  | Near-zero half life yields no attenuation (factor≈base)                             | robustness/test_robustness.py:792         |                                                                                                              |
-| pbrs-canonical-exit-semantic-106              | pbrs        | Canonical exit uses shaping=-prev_potential and next_potential=0.0                   | pbrs/test_pbrs.py:374                     | 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:42          | Docstring line                                                                                              |
-| helpers-transitions-preserve-multiplicity-108 | helpers     | Repeated transitions retain their empirical multiplicity                            | helpers/test_utilities.py:26               |                                                                                                             |
-| helpers-missing-cols-fill-109                 | helpers     | Missing required columns filled with NaN + single warning                            | helpers/test_utilities.py:51               | Docstring line                                                                                              |
-| statistics-binned-stats-min-edges-110         | statistics  | <2 bin edges raises ValueError                                                       | statistics/test_statistics.py:60          | Docstring line                                                                                              |
-| statistics-constant-cols-exclusion-111        | statistics  | Constant columns excluded & listed                                                   | statistics/test_statistics.py:71          | Docstring line                                                                                              |
-| statistics-degenerate-distribution-shift-112  | statistics  | Constants: zero distances; KS p only with declared independent observations          | statistics/test_statistics.py:87          | Docstring line                                                                                              |
-| statistics-constant-dist-exact-ci-113a        | statistics  | Both modes retain exact constant CI bounds                                           | statistics/test_statistics.py:606         |                                                                                                             |
-| statistics-percentile-outside-mean-113b       | statistics  | Percentile bounds need not contain the sample mean                                   | statistics/test_statistics.py:620         |                                                                                                             |
-| statistics-constant-diagnostics-115           | statistics  | Constants have N/A higher moments, normality tests and Q-Q fits in both modes         | statistics/test_statistics.py:194         |                                                                                                             |
-| pbrs-canonical-near-zero-report-116           | pbrs        | Canonical trajectories with valid evidence are classified as verified                | pbrs/test_pbrs.py:1509                    | 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:127         | Comment line                                                                                                |
-| pbrs-absence-shift-placeholder-118            | pbrs        | Placeholder shift line present when shaping shift is absent                          | pbrs/test_pbrs.py:1877                    |                                                                                                             |
-| components-pbrs-breakdown-fields-119          | components  | PBRS breakdown fields finite and mathematically aligned                              | components/test_reward_components.py:830  | 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:137 |                                                                                                             |
-| cli-pbrs-csv-columns-121                      | cli         | PBRS columns in reward_samples.csv when shaping enabled                              | cli/test_cli_params_and_csv.py:347        | 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:499         |                                                                                                             |
-| statistics-independence-contract-123          | statistics  | Inferential helpers require independent_observations=True                            | statistics/test_statistics.py:636         | 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:28  | 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:1699                    | Discontinuous potentials are not verified                                                                   |
+| ID                                            | Category    | Description                                                                         | Owning File                               | Notes                                                                                                                                                                          |
+| --------------------------------------------- | ----------- | ----------------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| report-additives-deterministic-092            | components  | Additives deterministic report section                                              | components/test_additives.py:4            | 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:43          | 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:654         | Comment line (function at :655)                                                                                                                                                |
+| robustness-negative-grace-clamp-103           | robustness  | Negative exit_plateau_grace clamps to 0.0 w/ warning                                | robustness/test_robustness.py:696         |                                                                                                                                                                                |
+| robustness-invalid-power-tau-104              | robustness  | Invalid power tau falls back alpha=1.0 w/ warning                                   | robustness/test_robustness.py:747         |                                                                                                                                                                                |
+| robustness-near-zero-half-life-105            | robustness  | Near-zero half life yields no attenuation (factor≈base)                             | robustness/test_robustness.py:792         |                                                                                                                                                                                |
+| pbrs-canonical-exit-semantic-106              | pbrs        | Canonical exit uses shaping=-prev_potential and next_potential=0.0                  | pbrs/test_pbrs.py:374                     | 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:42          | Docstring line                                                                                                                                                                 |
+| helpers-transitions-preserve-multiplicity-108 | helpers     | Repeated transitions retain their empirical multiplicity                            | helpers/test_utilities.py:26              |                                                                                                                                                                                |
+| helpers-missing-cols-fill-109                 | helpers     | Missing required columns filled with NaN + single warning                           | helpers/test_utilities.py:51              | Docstring line                                                                                                                                                                 |
+| statistics-binned-stats-min-edges-110         | statistics  | <2 bin edges raises ValueError                                                      | statistics/test_statistics.py:60          | Docstring line                                                                                                                                                                 |
+| statistics-constant-cols-exclusion-111        | statistics  | Constant columns excluded & listed                                                  | statistics/test_statistics.py:71          | Docstring line                                                                                                                                                                 |
+| statistics-degenerate-distribution-shift-112  | statistics  | Constants: zero distances; KS p only with declared independent observations         | statistics/test_statistics.py:87          | Docstring line                                                                                                                                                                 |
+| statistics-constant-dist-exact-ci-113a        | statistics  | Both modes retain exact constant CI bounds                                          | statistics/test_statistics.py:606         |                                                                                                                                                                                |
+| statistics-percentile-outside-mean-113b       | statistics  | Percentile bounds need not contain the sample mean                                  | statistics/test_statistics.py:620         |                                                                                                                                                                                |
+| statistics-constant-diagnostics-115           | statistics  | Constants have N/A higher moments, normality tests and Q-Q fits in both modes       | statistics/test_statistics.py:194         |                                                                                                                                                                                |
+| pbrs-canonical-near-zero-report-116           | pbrs        | Canonical trajectories with valid evidence are classified as verified               | pbrs/test_pbrs.py:1509                    | 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:127         | Comment line                                                                                                                                                                   |
+| pbrs-absence-shift-placeholder-118            | pbrs        | Placeholder shift line present when shaping shift is absent                         | pbrs/test_pbrs.py:1877                    |                                                                                                                                                                                |
+| components-pbrs-breakdown-fields-119          | components  | PBRS breakdown fields finite and mathematically aligned                             | components/test_reward_components.py:830  | 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:137 |                                                                                                                                                                                |
+| cli-pbrs-csv-columns-121                      | cli         | PBRS columns in reward_samples.csv when shaping enabled                             | cli/test_cli_params_and_csv.py:347        | 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:499         |                                                                                                                                                                                |
+| statistics-independence-contract-123          | statistics  | Inferential helpers require independent_observations=True                           | statistics/test_statistics.py:636         | 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:28  | 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:1699                    | Discontinuous potentials are not verified                                                                                                                                      |
 
 ### Non-Owning Smoke / Reference Checks
 
@@ -220,14 +220,14 @@ the invariant must include a leading comment:
 
 Table tracks approximate line ranges and source ownership:
 
-| File                                   | Lines (approx) | References                                               | Ownership Source                                                    |
-| -------------------------------------- | -------------- | -------------------------------------------------------- | ------------------------------------------------------------------- |
-| integration/test_reward_calculation.py | 44             | Decomposition identity (sum components)                  | robustness/test_robustness.py:43                                    |
-| components/test_reward_components.py   | 551            | Exit factor finiteness & plateau behavior                | robustness/test_robustness.py:43+                                   |
-| pbrs/test_pbrs.py                      | 1053           | Canonical vs non-canonical classification formatting     | robustness/test_robustness.py:43, robustness/test_robustness.py:127 |
-| pbrs/test_pbrs.py                      | 1415           | Non-canonical full report reason aggregation             | robustness/test_robustness.py:43                                    |
-| pbrs/test_pbrs.py                      | 1469           | Non-canonical mode-only reason (additives disabled)      | robustness/test_robustness.py:43                                    |
-| statistics/test_statistics.py          | 292            | Mean decomposition consistency                           | robustness/test_robustness.py:43                                    |
+| File                                   | Lines (approx) | References                                           | Ownership Source                                                    |
+| -------------------------------------- | -------------- | ---------------------------------------------------- | ------------------------------------------------------------------- |
+| integration/test_reward_calculation.py | 44             | Decomposition identity (sum components)              | robustness/test_robustness.py:43                                    |
+| components/test_reward_components.py   | 551            | Exit factor finiteness & plateau behavior            | robustness/test_robustness.py:43+                                   |
+| pbrs/test_pbrs.py                      | 1053           | Canonical vs non-canonical classification formatting | robustness/test_robustness.py:43, robustness/test_robustness.py:127 |
+| pbrs/test_pbrs.py                      | 1415           | Non-canonical full report reason aggregation         | robustness/test_robustness.py:43                                    |
+| pbrs/test_pbrs.py                      | 1469           | Non-canonical mode-only reason (additives disabled)  | robustness/test_robustness.py:43                                    |
+| statistics/test_statistics.py          | 292            | Mean decomposition consistency                       | robustness/test_robustness.py:43                                    |
 
 ### Deprecated / Reserved IDs
 
index c4f73c68ee90e47782069d47df5de30af3aae57d..8236e55ea7fa4dfe66c8b379b292072b66887dbc 100644 (file)
@@ -1,7 +1,9 @@
 #!/usr/bin/env python3
 """CLI-level tests: CSV encoding and parameter propagation."""
 
+import hashlib
 import json
+import pickle
 import subprocess
 import sys
 import unittest
@@ -10,7 +12,11 @@ from pathlib import Path
 import pandas as pd
 import pytest
 
-from reward_space_analysis import Actions
+from reward_space_analysis import (
+    DEFAULT_MODEL_REWARD_PARAMETERS,
+    Actions,
+    get_max_idle_duration_candles,
+)
 
 from ..constants import SCENARIOS, SEEDS, TOLERANCE
 from ..test_base import RewardSpaceTestBase
@@ -100,27 +106,91 @@ class TestParamsPropagation(RewardSpaceTestBase):
         fi_path = out_dir / "feature_importance.csv"
         self.assertFalse(fi_path.exists(), "feature_importance.csv should be absent when skipped")
 
-    def test_manifest_params_hash_generation(self):
-        """Ensure params_hash appears when non-default simulation params differ (risk_reward_ratio altered)."""
+    def test_manifest_records_resolved_simulation_inputs(self):
+        """The manifest records and hashes resolved simulation inputs."""
         out_dir = self.output_path / "manifest_hash"
-        result = _run_cli(
-            out_dir=out_dir,
+        explicit_defaults_dir = self.output_path / "explicit_defaults"
+        different_idle_dir = self.output_path / "different_idle"
+        different_seed_dir = self.output_path / "different_seed"
+        default_duration = DEFAULT_MODEL_REWARD_PARAMETERS["max_trade_duration_candles"]
+        default_idle = get_max_idle_duration_candles(DEFAULT_MODEL_REWARD_PARAMETERS)
+        common_args = [
+            "--num_samples",
+            str(SCENARIOS.CLI_NUM_SAMPLES_HASH),
+            "--seed",
+            str(SEEDS.BASE),
+            "--risk_reward_ratio",
+            str(SCENARIOS.CLI_RISK_REWARD_RATIO_NON_DEFAULT),
+            "--skip_feature_analysis",
+            "--skip_partial_dependence",
+        ]
+        result = _run_cli(out_dir=out_dir, args=common_args)
+        explicit_result = _run_cli(
+            out_dir=explicit_defaults_dir,
             args=[
-                "--num_samples",
-                str(SCENARIOS.CLI_NUM_SAMPLES_HASH),
-                "--seed",
-                str(SEEDS.BASE),
-                "--risk_reward_ratio",
-                str(SCENARIOS.CLI_RISK_REWARD_RATIO_NON_DEFAULT),
+                *common_args,
+                "--max_trade_duration_candles",
+                str(default_duration),
+                "--max_idle_duration_candles",
+                str(default_idle),
+                "--exit_plateau",
+                str(int(DEFAULT_MODEL_REWARD_PARAMETERS["exit_plateau"])),
             ],
         )
-        _assert_cli_success(self, result)
-        manifest_path = out_dir / "manifest.json"
-        self.assertTrue(manifest_path.exists(), "Missing manifest.json")
-        manifest = json.loads(manifest_path.read_text())
-        self.assertIn("params_hash", manifest, "params_hash should be present when params differ")
-        self.assertIn("simulation_params", manifest)
-        self.assertIn("risk_reward_ratio", manifest["simulation_params"])
+        different_result = _run_cli(
+            out_dir=different_idle_dir,
+            args=[*common_args, "--max_idle_duration_candles", str(default_idle + 1)],
+        )
+        different_seed_result = _run_cli(
+            out_dir=different_seed_dir,
+            args=[*common_args, "--seed", str(SEEDS.BASE + 1)],
+        )
+        for cli_result in (result, explicit_result, different_result, different_seed_result):
+            _assert_cli_success(self, cli_result)
+        manifest = json.loads((out_dir / "manifest.json").read_text())
+        explicit_manifest = json.loads((explicit_defaults_dir / "manifest.json").read_text())
+        different_manifest = json.loads((different_idle_dir / "manifest.json").read_text())
+        different_seed_manifest = json.loads((different_seed_dir / "manifest.json").read_text())
+        simulation_params = manifest["simulation_params"]
+        self.assertEqual(simulation_params["out_dir"], str(out_dir))
+        self.assertEqual(
+            manifest["effective"]["risk_reward_ratio"],
+            SCENARIOS.CLI_RISK_REWARD_RATIO_NON_DEFAULT,
+        )
+        self.assertEqual(manifest["reward_params"]["max_idle_duration_candles"], default_idle)
+        self.assertIsNone(simulation_params["real_episodes_sha256"])
+        boolean_keys = {
+            key for key, value in DEFAULT_MODEL_REWARD_PARAMETERS.items() if isinstance(value, bool)
+        }
+        self.assertTrue(boolean_keys.isdisjoint(manifest["parameter_adjustments"]))
+        plateau_adjustment = explicit_manifest["parameter_adjustments"]["exit_plateau"]
+        self.assertIs(
+            plateau_adjustment["adjusted"], DEFAULT_MODEL_REWARD_PARAMETERS["exit_plateau"]
+        )
+        reward_keys = set(manifest["reward_params"])
+        effective_keys = set(manifest["effective"])
+        self.assertTrue(reward_keys.isdisjoint(simulation_params))
+        self.assertTrue(effective_keys.isdisjoint(simulation_params))
+        self.assertTrue(reward_keys.isdisjoint(effective_keys))
+        pd.testing.assert_frame_equal(
+            pd.read_csv(out_dir / "reward_samples.csv"),
+            pd.read_csv(explicit_defaults_dir / "reward_samples.csv"),
+        )
+        self.assertEqual(manifest["params_hash"], explicit_manifest["params_hash"])
+        self.assertNotEqual(manifest["params_hash"], different_manifest["params_hash"])
+        self.assertNotEqual(manifest["params_hash"], different_seed_manifest["params_hash"])
+
+    def test_manifest_write_failure_fails_cli(self):
+        """A required manifest write failure makes the CLI fail."""
+        out_dir = self.output_path / "invalid_manifest_target"
+        out_dir.mkdir()
+        (out_dir / "manifest.json").mkdir()
+        result = _run_cli(
+            out_dir=out_dir,
+            args=["--num_samples", "20", "--skip_feature_analysis", "--skip_partial_dependence"],
+        )
+        self.assertNotEqual(result.returncode, 0)
+        self.assertIn("Manifest generation failed", result.stderr + result.stdout)
 
     def test_pbrs_invariance_section_present(self):
         """When reward_shaping column exists, summary should include PBRS invariance section."""
@@ -216,10 +286,12 @@ class TestParamsPropagation(RewardSpaceTestBase):
         self.assertIn(str(invalid), result.stderr + result.stdout)
         self.assertFalse(out_dir.exists())
 
-    def test_valid_real_episodes_produce_real_metrics(self):
-        """A valid episodes pickle loads before simulation and enables real metrics."""
+    def test_real_episode_content_identifies_manifest_hash(self):
+        """The hash identifies episode bytes, independently of their file path."""
         out_dir = self.output_path / "valid_real"
-        import pickle
+        copied_out_dir = self.output_path / "copied_real"
+        changed_out_dir = self.output_path / "changed_real"
+        synthetic_out_dir = self.output_path / "without_real"
 
         episodes = [
             {
@@ -237,41 +309,85 @@ class TestParamsPropagation(RewardSpaceTestBase):
             }
         ]
         episodes_path = self.output_path / "episodes.pkl"
-        with episodes_path.open("wb") as fh:
-            pickle.dump(episodes, fh)
-        result = _run_cli(
-            out_dir=out_dir,
-            args=["--num_samples", "50", "--real_episodes", str(episodes_path)],
+        episodes_path.write_bytes(pickle.dumps(episodes))
+        copied_episodes_path = self.output_path / "episodes_copy.pkl"
+        copied_episodes_path.write_bytes(episodes_path.read_bytes())
+        changed_episodes_path = self.output_path / "episodes_changed.pkl"
+        changed_episodes = pickle.loads(episodes_path.read_bytes())
+        changed_episodes[0]["transitions"][0]["reward"] = -1.0
+        changed_episodes_path.write_bytes(pickle.dumps(changed_episodes))
+
+        common_args = [
+            "--num_samples",
+            "50",
+            "--skip_feature_analysis",
+            "--skip_partial_dependence",
+        ]
+        runs = (
+            (out_dir, [*common_args, "--real_episodes", str(episodes_path)]),
+            (
+                copied_out_dir,
+                [*common_args, "--real_episodes", str(copied_episodes_path)],
+            ),
+            (
+                changed_out_dir,
+                [*common_args, "--real_episodes", str(changed_episodes_path)],
+            ),
+            (synthetic_out_dir, common_args),
         )
-        _assert_cli_success(self, result)
+        for run_out_dir, args in runs:
+            _assert_cli_success(self, _run_cli(out_dir=run_out_dir, args=args))
+
         report = (out_dir / "statistical_analysis.md").read_text(encoding="utf-8")
         self.assertNotIn("Not performed (no real episodes provided)", report)
-
-    def test_params_override_flags_and_manifest_reflects_effective(self):
-        """--params beats explicit flags; manifest effective values follow resolution."""
-        out_dir = self.output_path / "params_beat_flags"
-        result = _run_cli(
-            out_dir=out_dir,
+        manifest = json.loads((out_dir / "manifest.json").read_text())
+        copied_manifest = json.loads((copied_out_dir / "manifest.json").read_text())
+        changed_manifest = json.loads((changed_out_dir / "manifest.json").read_text())
+        synthetic_manifest = json.loads((synthetic_out_dir / "manifest.json").read_text())
+        simulation_params = manifest["simulation_params"]
+        expected_digest = hashlib.sha256(episodes_path.read_bytes()).hexdigest()
+        self.assertEqual(simulation_params["real_episodes"], str(episodes_path))
+        self.assertEqual(simulation_params["real_episodes_sha256"], expected_digest)
+        self.assertEqual(manifest["params_hash"], copied_manifest["params_hash"])
+        self.assertNotEqual(manifest["params_hash"], changed_manifest["params_hash"])
+        self.assertNotEqual(manifest["params_hash"], synthetic_manifest["params_hash"])
+
+    def test_hybrid_parameter_routes_have_one_canonical_hash(self):
+        """Overrides beat conflicting flags and match equivalent direct inputs."""
+        params_out_dir = self.output_path / "params_route"
+        flags_out_dir = self.output_path / "flags_route"
+        common_args = [
+            "--num_samples",
+            str(SCENARIOS.CLI_NUM_SAMPLES_FAST),
+            "--base_factor",
+            "150.0",
+            "--skip_feature_analysis",
+            "--skip_partial_dependence",
+        ]
+        params_result = _run_cli(
+            out_dir=params_out_dir,
             args=[
-                "--num_samples",
-                str(SCENARIOS.CLI_NUM_SAMPLES_FAST),
+                *common_args,
                 "--profit_aim",
-                "0.05",
-                "--base_factor",
-                "150.0",
+                "0.04",
+                "--risk_reward_ratio",
+                "3.0",
                 "--params",
                 "profit_aim=0.02",
                 "risk_reward_ratio=1.5",
             ],
         )
-        _assert_cli_success(self, result)
-        with (out_dir / "manifest.json").open() as f:
-            manifest = json.load(f)
-        effective = manifest["effective"]
-        self.assertEqual(effective["profit_aim"], 0.02)
-        self.assertEqual(effective["risk_reward_ratio"], 1.5)
-        self.assertEqual(effective["base_factor"], 150.0)
-        self.assertAlmostEqual(manifest["pnl_target"], 0.03)
+        flags_result = _run_cli(
+            out_dir=flags_out_dir,
+            args=[*common_args, "--profit_aim", "0.02", "--risk_reward_ratio", "1.5"],
+        )
+        _assert_cli_success(self, params_result)
+        _assert_cli_success(self, flags_result)
+        params_manifest = json.loads((params_out_dir / "manifest.json").read_text())
+        flags_manifest = json.loads((flags_out_dir / "manifest.json").read_text())
+        self.assertEqual(params_manifest["effective"], flags_manifest["effective"])
+        self.assertEqual(params_manifest["params_hash"], flags_manifest["params_hash"])
+        self.assertAlmostEqual(params_manifest["pnl_target"], 0.03)
 
     def test_simulation_only_params_rejected_before_artifacts(self):
         """Simulation-only keys fail the run before any artifact is written."""
index efd4d87390bcb1c5a53a163dcaea99308dcac383..1327359aa3056e2c6240f1c336f233a2faf27de5 100644 (file)
       "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
-      "plot_new_best": true // Enable tensorboard rollout plot upon finding a new best model
+      "plot_new_best": false // Disabled with multiprocessing; enable only when multiprocessing is false
     },
     "rl_config_optuna": {
       "enabled": true, // Enable optuna hyperopt