]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
fix(reforcexy): clarify diagnostics and deduplicate leverage warnings
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Wed, 23 Sep 2026 23:20:30 +0000 (01:20 +0200)
committerJérôme Benoit <jerome.benoit@piment-noir.org>
Wed, 23 Sep 2026 23:20:30 +0000 (01:20 +0200)
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/integration/test_report_formatting.py
ReforceXY/tests/test_review_contracts.py
ReforceXY/user_data/strategies/RLAgentStrategy.py

index 61c476f94cbc401e187b6ff64778131d4949f9da..1bacced3e8b041e2cc17492ee4243672f721bca4 100644 (file)
     },
     {
       "endCharacter": 5,
-      "endLine": 2135,
+      "endLine": 2136,
       "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": 2130
+      "startLine": 2131
     },
     {
       "endCharacter": 21,
-      "endLine": 2132,
+      "endLine": 2133,
       "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": 2132
+      "startLine": 2133
     },
     {
       "endCharacter": 5,
-      "endLine": 2157,
+      "endLine": 2158,
       "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": 2153
+      "startLine": 2154
     },
     {
       "endCharacter": 43,
-      "endLine": 2154,
+      "endLine": 2155,
       "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": 2154
+      "startLine": 2155
     },
     {
       "endCharacter": 42,
-      "endLine": 2154,
+      "endLine": 2155,
       "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": 2154
+      "startLine": 2155
     },
     {
       "endCharacter": 60,
-      "endLine": 2414,
+      "endLine": 2415,
       "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": 2414
+      "startLine": 2415
     },
     {
       "endCharacter": 58,
-      "endLine": 2415,
+      "endLine": 2416,
       "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": 2415
+      "startLine": 2416
     },
     {
       "endCharacter": 88,
-      "endLine": 2434,
+      "endLine": 2435,
       "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": 2434
+      "startLine": 2435
     },
     {
       "endCharacter": 88,
-      "endLine": 2434,
+      "endLine": 2435,
       "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": 2434
+      "startLine": 2435
     },
     {
       "endCharacter": 17,
-      "endLine": 2443,
+      "endLine": 2444,
       "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
       "message": "Object of type \"None\" cannot be called",
       "rule": "reportOptionalCall",
       "severity": "error",
       "startCharacter": 28,
-      "startLine": 2437
+      "startLine": 2438
     },
     {
       "endCharacter": 29,
-      "endLine": 3038,
+      "endLine": 3043,
       "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": 3038
+      "startLine": 3043
     },
     {
       "endCharacter": 28,
-      "endLine": 3038,
+      "endLine": 3043,
       "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": 3038
+      "startLine": 3043
     },
     {
       "endCharacter": 65,
-      "endLine": 3047,
+      "endLine": 3052,
       "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": 3047
+      "startLine": 3052
     },
     {
       "endCharacter": 65,
-      "endLine": 3047,
+      "endLine": 3052,
       "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": 3047
+      "startLine": 3052
     },
     {
       "endCharacter": 5,
-      "endLine": 3947,
+      "endLine": 3952,
       "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": 3943
+      "startLine": 3948
     },
     {
       "endCharacter": 43,
-      "endLine": 3944,
+      "endLine": 3949,
       "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": 3944
+      "startLine": 3949
     },
     {
       "endCharacter": 42,
-      "endLine": 3944,
+      "endLine": 3949,
       "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": 3944
+      "startLine": 3949
     },
     {
       "endCharacter": 22,
     },
     {
       "endCharacter": 17,
-      "endLine": 62,
+      "endLine": 63,
       "file": "ReforceXY/user_data/strategies/RLAgentStrategy.py",
       "message": "\"can_short\" overrides symbol of same name in class \"IStrategy\"\n  \"property\" is not assignable to \"bool\"",
       "rule": "reportIncompatibleVariableOverride",
       "severity": "error",
       "startCharacter": 8,
-      "startLine": 62
+      "startLine": 63
     },
     {
       "endCharacter": 19,
-      "endLine": 66,
+      "endLine": 67,
       "file": "ReforceXY/user_data/strategies/RLAgentStrategy.py",
       "message": "\"protections\" overrides symbol of same name in class \"IStrategy\"\n  \"property\" is not assignable to \"list[Unknown]\"",
       "rule": "reportIncompatibleVariableOverride",
       "severity": "error",
       "startCharacter": 8,
-      "startLine": 66
+      "startLine": 67
     },
     {
       "endCharacter": 72,
-      "endLine": 88,
+      "endLine": 89,
       "file": "ReforceXY/user_data/strategies/RLAgentStrategy.py",
       "message": "No overloads for \"__call__\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 42,
-      "startLine": 88
+      "startLine": 89
     },
     {
       "endCharacter": 71,
-      "endLine": 88,
+      "endLine": 89,
       "file": "ReforceXY/user_data/strategies/RLAgentStrategy.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"x1\" of type \"_SupportsArrayUFunc\" in function \"__call__\"\n  Type \"Series[Any] | None\" is not assignable to type \"_SupportsArrayUFunc\"\n    \"None\" is incompatible with protocol \"_SupportsArrayUFunc\"\n      \"__array_ufunc__\" is not present",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 49,
-      "startLine": 88
+      "startLine": 89
     }
   ],
   "filesAnalyzed": 3,
index 0df5b6fb47e2857e284f2167e78b67ed448fbc63..5000b4e919cff96a5f65110ac318af0f10dfb16a 100644 (file)
@@ -593,8 +593,9 @@ Implementation: up to 50 evenly spaced histogram edges (normally 49 bins) with
 
 Non-finite numeric values in real episodes are marked missing. Each feature is
 compared only when both synthetic and real data contain at least 10 finite
-observations; otherwise it is omitted. If none qualify, the report distinguishes
-this from not supplying real episodes.
+observations; otherwise it is omitted. When none qualify, the report distinguishes
+missing episodes, no comparable finite data, and fewer than 10 finite observations
+per dataset and comparable feature.
 
 ---
 
index ed6b312fd754f1a2bec6697387ad0cc1f75cb3ac..c4f6854ba276d9316088393c1bd61e1d0f79eeb0 100644 (file)
@@ -1988,7 +1988,7 @@ def _validate_simulation_invariants(df: pd.DataFrame, params: RewardParams) -> N
     )
 
     # INVARIANT 1: masked sampling must only emit legal actions; unmasked
-    # actions may be invalid but cannot alter an open position.
+    # actions may be invalid but cannot change position before terminal liquidation.
     if _get_bool_param(params, "action_masking", True):
         long_exits = df[(df["action"] == 2.0) & (df["position"] != 1.0)]
         short_exits = df[(df["action"] == 4.0) & (df["position"] != 0.0)]
@@ -2016,9 +2016,7 @@ def _validate_simulation_invariants(df: pd.DataFrame, params: RewardParams) -> N
             & df["next_position"].ne(df["position"])
         ]
         if len(changed_on_invalid) > 0:
-            raise AssertionError(
-                f"Sim: {len(changed_on_invalid)} invalid actions changed open position"
-            )
+            raise AssertionError(f"Sim: {len(changed_on_invalid)} invalid actions changed position")
 
     # INVARIANT 2: Duration logic
     neutral_with_trade = df[(df["position"] == 0.5) & (df["trade_duration"] > 0)]
@@ -2658,6 +2656,10 @@ def _stabilize_divergence(value: float, *, bin_count: int, metric_name: str) ->
     return value
 
 
+_DISTRIBUTION_SHIFT_FEATURES: Final[tuple[str, ...]] = ("pnl", "trade_duration", "idle_duration")
+_MIN_DISTRIBUTION_SHIFT_OBSERVATIONS: Final[int] = 10
+
+
 def compute_distribution_shift_metrics(
     synthetic_df: pd.DataFrame,
     real_df: pd.DataFrame,
@@ -2673,7 +2675,7 @@ def compute_distribution_shift_metrics(
     reported. Constants yield exact zero distances and, inferentially, p=1.0.
     """
     metrics = {}
-    continuous_features = ["pnl", "trade_duration", "idle_duration"]
+    continuous_features = _DISTRIBUTION_SHIFT_FEATURES
 
     for feature in continuous_features:
         synth_values = synthetic_df[feature].to_numpy(dtype=float, na_value=np.nan)
@@ -2685,7 +2687,7 @@ def compute_distribution_shift_metrics(
         if not real_finite.all():
             real_values = real_values[real_finite]
 
-        if len(synth_values) < 10 or len(real_values) < 10:
+        if min(len(synth_values), len(real_values)) < _MIN_DISTRIBUTION_SHIFT_OBSERVATIONS:
             continue
 
         min_val = min(synth_values.min(), real_values.min())
@@ -4107,6 +4109,20 @@ def write_complete_statistical_analysis(
         "no real episodes provided" if real_df is None else "no comparable finite observations"
     )
 
+    if (
+        real_df is not None
+        and not distribution_shift
+        and any(
+            np.isfinite(df[feature].to_numpy(dtype=float, na_value=np.nan)).any()
+            and np.isfinite(real_df[feature].to_numpy(dtype=float, na_value=np.nan)).any()
+            for feature in _DISTRIBUTION_SHIFT_FEATURES
+        )
+    ):
+        distribution_shift_unavailable = (
+            "insufficient finite observations; "
+            f"at least {_MIN_DISTRIBUTION_SHIFT_OBSERVATIONS} per dataset and feature required"
+        )
+
     # Write comprehensive report
     with report_path.open("w", encoding="utf-8") as f:
         # Header
index 560ec9d237f7a80ab7f562f694a3e0d7a012cf6d..4b0bc00e42739643f51c78d752cc85619572c65f 100644 (file)
@@ -181,53 +181,52 @@ Columns:
   stable across unrelated line insertions.
 - Notes: Sub-modes, non-owning references and multi-path coverage.
 
-| ID                                            | Category    | Description                                                                          | Owning test                                                                                                       | Notes                                                                                                                                                                          |
-| --------------------------------------------- | ----------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| report-additives-deterministic-092            | components  | Additives deterministic report section                                               | components/test_additives.py::test_additive_activation_deterministic_contribution                                 | Integration/PBRS may reference outcome non-owning                                                                                                                              |
-| robustness-decomposition-integrity-101        | robustness  | Single active core component equals total reward under mutually exclusive scenarios  | robustness/test_robustness.py::test_decomposition_integrity                                                       | Scenarios: idle, hold, exit, invalid; non-owning refs integration/test_reward_calculation.py                                                                                   |
-| robustness-exit-mode-fallback-102             | robustness  | Unknown exit_attenuation_mode falls back to linear w/ warning                        | robustness/test_robustness.py::test_robustness_102_unknown_exit_mode_fallback_linear                              |                                                                                                                                                                                |
-| robustness-negative-grace-clamp-103           | robustness  | Negative exit_plateau_grace clamps to 0.0 w/ warning                                 | robustness/test_robustness.py::test_robustness_103_negative_plateau_grace_clamped                                 |                                                                                                                                                                                |
-| robustness-invalid-power-tau-104              | robustness  | Invalid power tau falls back alpha=1.0 w/ warning                                    | robustness/test_robustness.py::test_robustness_104_invalid_power_tau_fallback_alpha_one                           |                                                                                                                                                                                |
-| robustness-near-zero-half-life-105            | robustness  | Near-zero half life yields no attenuation (factor≈base)                              | robustness/test_robustness.py::test_robustness_105_half_life_near_zero_fallback                                   |                                                                                                                                                                                |
-| pbrs-canonical-exit-semantic-106              | pbrs        | Canonical exit uses shaping=-prev_potential and next_potential=0.0                   | pbrs/test_pbrs.py::test_exit_step_shaping_matches_exit_step_rules                                                 | Uses stored potential across steps; no drift correction applied                                                                                                                |
-| statistics-partial-deps-skip-107              | statistics  | skip_partial_dependence => empty PD structures                                       | statistics/test_statistics.py::test_statistics_feature_analysis_skip_partial_dependence                           |                                                                                                                                                                                |
-| helpers-transitions-preserve-multiplicity-108 | helpers     | Repeated transitions retain their empirical multiplicity                             | helpers/test_utilities.py::test_repeated_transitions_preserve_multiplicity                                        |                                                                                                                                                                                |
-| helpers-missing-cols-fill-109                 | helpers     | Missing required columns filled with NaN + single warning                            | helpers/test_utilities.py::test_missing_multiple_required_columns_single_warning                                  |                                                                                                                                                                                |
-| statistics-binned-stats-min-edges-110         | statistics  | <2 bin edges raises ValueError                                                       | statistics/test_statistics.py::test_statistics_binned_stats_invalid_bins_raises                                   | Docstring line                                                                                                                                                                 |
-| statistics-constant-cols-exclusion-111        | statistics  | Constant columns excluded & listed                                                   | statistics/test_statistics.py::test_statistics_correlation_dropped_constant_columns                               | Docstring line                                                                                                                                                                 |
-| statistics-degenerate-distribution-shift-112  | statistics  | Constants: zero distances; KS p only with declared independent observations          | statistics/test_statistics.py::test_statistics_distribution_shift_metrics_degenerate_zero                         | Docstring line                                                                                                                                                                 |
-| statistics-constant-dist-exact-ci-113a        | statistics  | Both modes retain exact constant CI bounds                                           | statistics/test_statistics.py::test_stats_bootstrap_constant_distribution_exact_bounds                            |                                                                                                                                                                                |
-| statistics-percentile-outside-mean-113b       | statistics  | Percentile bounds need not contain the sample mean                                   | statistics/test_statistics.py::test_stats_bootstrap_percentiles_need_not_contain_mean                             |                                                                                                                                                                                |
-| statistics-constant-diagnostics-115           | statistics  | Constants have N/A higher moments, normality tests and Q-Q fits in both modes        | statistics/test_statistics.py::test_statistics_distribution_constant_diagnostics                                  |                                                                                                                                                                                |
-| pbrs-canonical-near-zero-report-116           | pbrs        | Canonical trajectories with valid evidence are classified as verified                | pbrs/test_pbrs.py::test_pbrs_canonical_near_zero_report                                                           | Requires local identity, continuity, discounted terminal boundary, and zero observed additives; the non-owning boundary test also covers a complete singleton terminal episode |
-| robustness-exit-pnl-only-117                  | robustness  | Only exit actions have non-zero PnL                                                  | robustness/test_robustness.py::test_pnl_invariant_exit_only                                                       |                                                                                                                                                                                |
-| pbrs-absence-shift-placeholder-118            | pbrs        | Placeholder shift line present when shaping shift is absent                          | pbrs/test_pbrs.py::test_pbrs_absence_and_distribution_shift_placeholder                                           |                                                                                                                                                                                |
-| components-pbrs-breakdown-fields-119          | components  | PBRS breakdown fields finite and mathematically aligned                              | components/test_reward_components.py::test_pbrs_breakdown_fields_finite_and_aligned                               | Tests base_reward, pbrs_delta and invariance_correction alignment                                                                                                              |
-| integration-pbrs-metrics-section-120          | integration | PBRS Metrics section present in report with tracing metrics                          | integration/test_report_formatting.py::test_report_includes_pbrs_metrics_section                                  |                                                                                                                                                                                |
-| cli-pbrs-csv-columns-121                      | cli         | PBRS columns in reward_samples.csv when shaping enabled                              | cli/test_cli_params_and_csv.py::test_csv_contains_pbrs_columns_when_shaping_present                               | Verifies finite reward_base, reward_pbrs_delta and reward_invariance_correction values                                                                                         |
-| statistics-bh-finite-family-122               | statistics  | Undefined tests excluded from finite-only BH family; marked non-applicable           | statistics/test_statistics.py::test_bh_excludes_undefined_tests_from_finite_family                                |                                                                                                                                                                                |
-| statistics-independence-contract-123          | statistics  | Inferential helpers require independent_observations=True                            | statistics/test_statistics.py::test_inference_helpers_require_independent_observations                            | Covers hypothesis tests and bootstrap intervals                                                                                                                                |
-| report-independent-sections-124               | integration | CI, diagnostics and shift sections do not depend on hypothesis-test output           | integration/test_report_formatting.py::test_statistical_sections_do_not_depend_on_hypothesis_tests                | Also verifies the reported bootstrap resample count                                                                                                                            |
-| pbrs-discounted-evidence-125                  | pbrs        | Verification requires local identity, continuity and discounted terminal boundary    | pbrs/test_pbrs.py::test_pbrs_canonical_discontinuous_potentials_report                                            | Discontinuous potentials are not verified                                                                                                                                      |
-| statistics-proportional-histograms-126        | statistics  | Proportional histograms ignore sample count; moved mass yields positive KL/JS        | statistics/test_statistics.py::test_distribution_shift_proportional_histograms_ignore_sample_count                | KL and JS remain finite and non-negative                                                                                                                                       |
-| pbrs-near-bound-clamp-127                     | pbrs        | Relaxed near-bound clamps apply exact endpoints and retain all reasons               | pbrs/test_pbrs.py::test_validate_reward_parameters_records_near_bound_clamps_exactly                              | Includes numeric-string coercion                                                                                                                                               |
-| pbrs-exit-mode-validation-128                 | pbrs        | Exit-potential choices are strict or canonicalized; direct calls fail safe           | pbrs/test_pbrs.py::test_invalid_exit_mode_warns_at_direct_and_simulation_boundaries                               | Direct calculation and simulation boundaries warn before fallback; PBRS calls suppress additives                                                                               |
-| cli-invalid-exit-mode-129                     | cli         | Invalid `--params exit_potential_mode` fails before artifacts                        | cli/test_cli_params_and_csv.py::test_invalid_exit_potential_mode_params_fails_before_artifacts                    | Strict CLI validation                                                                                                                                                          |
-| cli-warning-header-recognition-130            | cli         | Warning counts accept only anchored Python warning header formats                    | cli/test_cli_params_and_csv.py::test_warning_header_positive_and_negative_formats                                 | Covers POSIX, relative, synthetic and Windows source locations                                                                                                                 |
-| pbrs-invalid-mode-provenance-131              | pbrs        | Invalid imported exit-mode metadata cannot certify canonical invariance              | pbrs/test_pbrs.py::test_report_rejects_invalid_exit_mode_provenance                                               | Preserves the invalid raw value and reports effective additive settings as unknown                                                                                             |
-| pbrs-synthetic-fee-floor-132                  | pbrs        | High-fee entry loss remains in long/short synthetic PnL and exit rewards             | pbrs/test_pbrs.py::test_synthetic_fee_loss_extrema_match_retained_pnl                                             | Direct and transformed trajectories                                                                                                                                            |
-| pbrs-synthetic-profitable-mark-133            | pbrs        | Favorable long/short marks remain profitable after high-fee synthetic transformation | pbrs/test_pbrs.py::test_synthetic_high_fee_winner_retains_profitable_mark                                         | Covers immediate gains and recovery after an initial loss on the independent sampled market path                                                                               |
-| pbrs-synthetic-fee-boundary-134               | pbrs        | Fee-boundary rounding is accepted while materially extreme PnL is rejected           | pbrs/test_pbrs.py::test_synthetic_high_fee_short_boundary_rejects_real_excess                                     | Non-unit entry price exposes floating-point roundoff                                                                                                                           |
-| pbrs-synthetic-latent-price-135               | pbrs        | Clipped candidate PnL does not erase the sampled market path for later holds         | pbrs/test_pbrs.py::test_unrealized_pnl_retains_sampled_market_path_after_candidate_cap                            | Equal first retained marks from distinct market prices diverge after the same adverse return                                                                                   |
-| api-unmasked-invalid-actions-136              | api         | Unmasked invalid actions keep positions and receive penalties                        | api/test_api_helpers.py::test_unmasked_simulation_samples_invalid_actions_without_changing_position               | Includes spot, futures and zero-penalty invalid actions                                                                                                                        |
-| api-terminal-invalid-exit-137                 | api         | Wrong-side terminal exits liquidate the held side                                    | api/test_api_helpers.py::test_invalid_terminal_exit_liquidates_the_held_position                                  | Prevents dropped terminal trades                                                                                                                                               |
-| cli-stale-generated-artifacts-138             | cli         | Skipped analyses remove only reserved outputs from an earlier run                    | cli/test_cli_params_and_csv.py::test_skipped_analysis_removes_only_stale_generated_artifacts                      | Preserves unrelated files                                                                                                                                                      |
-| helpers-nonfinite-episodes-139                | helpers     | Infinite real observations become missing without dropping rows                      | helpers/test_utilities.py::test_nonfinite_numeric_episodes_are_marked_missing                                     | Retains transition multiplicity                                                                                                                                                |
-| statistics-finite-shift-140                   | statistics  | Finite observations remain comparable despite an infinite value                      | statistics/test_statistics.py::test_distribution_shift_uses_finite_observations                                   | Rejects all-nonfinite features                                                                                                                                                 |
-| statistics-rank-direction-141                 | statistics  | Rank-biserial effect follows the named first-group advantage                         | statistics/test_statistics.py::test_pnl_rank_biserial_direction_matches_named_first_group                         | Checks both directions                                                                                                                                                         |
-| statistics-bootstrap-count-142                | statistics  | Zero and negative resample counts fail for variable and constant data                | statistics/test_statistics.py::test_bootstrap_rejects_nonpositive_resample_count                                  | Rejects missing bootstrap                                                                                                                                                      |
-| integration-finite-report-143                 | integration | Reports distinguish unusable real values from missing real episodes                  | integration/test_report_formatting.py::test_report_distinguishes_missing_real_episodes_from_unusable_observations | Section and summary agree                                                                                                                                                      |
-| api-unmasked-sample-probabilities-144         | api         | Unmasked sample probabilities match marginal valid-action frequencies                | api/test_api_helpers.py::test_unmasked_sampling_probabilities_match_action_frequencies                            | Spot/futures entries, long/short exits, and neutral probability                                                                                                                |
+| ID                                            | Category    | Description                                                                          | Owning test                                                                                         | Notes                                                                                                                                                                          |
+| --------------------------------------------- | ----------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| report-additives-deterministic-092            | components  | Additives deterministic report section                                               | components/test_additives.py::test_additive_activation_deterministic_contribution                   | Integration/PBRS may reference outcome non-owning                                                                                                                              |
+| robustness-decomposition-integrity-101        | robustness  | Single active core component equals total reward under mutually exclusive scenarios  | robustness/test_robustness.py::test_decomposition_integrity                                         | Scenarios: idle, hold, exit, invalid; non-owning refs integration/test_reward_calculation.py                                                                                   |
+| robustness-exit-mode-fallback-102             | robustness  | Unknown exit_attenuation_mode falls back to linear w/ warning                        | robustness/test_robustness.py::test_robustness_102_unknown_exit_mode_fallback_linear                |                                                                                                                                                                                |
+| robustness-negative-grace-clamp-103           | robustness  | Negative exit_plateau_grace clamps to 0.0 w/ warning                                 | robustness/test_robustness.py::test_robustness_103_negative_plateau_grace_clamped                   |                                                                                                                                                                                |
+| robustness-invalid-power-tau-104              | robustness  | Invalid power tau falls back alpha=1.0 w/ warning                                    | robustness/test_robustness.py::test_robustness_104_invalid_power_tau_fallback_alpha_one             |                                                                                                                                                                                |
+| robustness-near-zero-half-life-105            | robustness  | Near-zero half life yields no attenuation (factor≈base)                              | robustness/test_robustness.py::test_robustness_105_half_life_near_zero_fallback                     |                                                                                                                                                                                |
+| pbrs-canonical-exit-semantic-106              | pbrs        | Canonical exit uses shaping=-prev_potential and next_potential=0.0                   | pbrs/test_pbrs.py::test_exit_step_shaping_matches_exit_step_rules                                   | Uses stored potential across steps; no drift correction applied                                                                                                                |
+| statistics-partial-deps-skip-107              | statistics  | skip_partial_dependence => empty PD structures                                       | statistics/test_statistics.py::test_statistics_feature_analysis_skip_partial_dependence             |                                                                                                                                                                                |
+| helpers-transitions-preserve-multiplicity-108 | helpers     | Repeated transitions retain their empirical multiplicity                             | helpers/test_utilities.py::test_repeated_transitions_preserve_multiplicity                          |                                                                                                                                                                                |
+| helpers-missing-cols-fill-109                 | helpers     | Missing required columns filled with NaN + single warning                            | helpers/test_utilities.py::test_missing_multiple_required_columns_single_warning                    |                                                                                                                                                                                |
+| statistics-binned-stats-min-edges-110         | statistics  | <2 bin edges raises ValueError                                                       | statistics/test_statistics.py::test_statistics_binned_stats_invalid_bins_raises                     | Docstring line                                                                                                                                                                 |
+| statistics-constant-cols-exclusion-111        | statistics  | Constant columns excluded & listed                                                   | statistics/test_statistics.py::test_statistics_correlation_dropped_constant_columns                 | Docstring line                                                                                                                                                                 |
+| statistics-degenerate-distribution-shift-112  | statistics  | Constants: zero distances; KS p only with declared independent observations          | statistics/test_statistics.py::test_statistics_distribution_shift_metrics_degenerate_zero           | Docstring line                                                                                                                                                                 |
+| statistics-constant-dist-exact-ci-113a        | statistics  | Both modes retain exact constant CI bounds                                           | statistics/test_statistics.py::test_stats_bootstrap_constant_distribution_exact_bounds              |                                                                                                                                                                                |
+| statistics-percentile-outside-mean-113b       | statistics  | Percentile bounds need not contain the sample mean                                   | statistics/test_statistics.py::test_stats_bootstrap_percentiles_need_not_contain_mean               |                                                                                                                                                                                |
+| statistics-constant-diagnostics-115           | statistics  | Constants have N/A higher moments, normality tests and Q-Q fits in both modes        | statistics/test_statistics.py::test_statistics_distribution_constant_diagnostics                    |                                                                                                                                                                                |
+| pbrs-canonical-near-zero-report-116           | pbrs        | Canonical trajectories with valid evidence are classified as verified                | pbrs/test_pbrs.py::test_pbrs_canonical_near_zero_report                                             | Requires local identity, continuity, discounted terminal boundary, and zero observed additives; the non-owning boundary test also covers a complete singleton terminal episode |
+| robustness-exit-pnl-only-117                  | robustness  | Only exit actions have non-zero PnL                                                  | robustness/test_robustness.py::test_pnl_invariant_exit_only                                         |                                                                                                                                                                                |
+| pbrs-absence-shift-placeholder-118            | pbrs        | Placeholder shift line present when shaping shift is absent                          | pbrs/test_pbrs.py::test_pbrs_absence_and_distribution_shift_placeholder                             |                                                                                                                                                                                |
+| components-pbrs-breakdown-fields-119          | components  | PBRS breakdown fields finite and mathematically aligned                              | components/test_reward_components.py::test_pbrs_breakdown_fields_finite_and_aligned                 | Tests base_reward, pbrs_delta and invariance_correction alignment                                                                                                              |
+| integration-pbrs-metrics-section-120          | integration | PBRS Metrics section present in report with tracing metrics                          | integration/test_report_formatting.py::test_report_includes_pbrs_metrics_section                    |                                                                                                                                                                                |
+| cli-pbrs-csv-columns-121                      | cli         | PBRS columns in reward_samples.csv when shaping enabled                              | cli/test_cli_params_and_csv.py::test_csv_contains_pbrs_columns_when_shaping_present                 | Verifies finite reward_base, reward_pbrs_delta and reward_invariance_correction values                                                                                         |
+| statistics-bh-finite-family-122               | statistics  | Undefined tests excluded from finite-only BH family; marked non-applicable           | statistics/test_statistics.py::test_bh_excludes_undefined_tests_from_finite_family                  |                                                                                                                                                                                |
+| statistics-independence-contract-123          | statistics  | Inferential helpers require independent_observations=True                            | statistics/test_statistics.py::test_inference_helpers_require_independent_observations              | Covers hypothesis tests and bootstrap intervals                                                                                                                                |
+| report-independent-sections-124               | integration | CI, diagnostics and shift sections do not depend on hypothesis-test output           | integration/test_report_formatting.py::test_statistical_sections_do_not_depend_on_hypothesis_tests  | Also verifies the reported bootstrap resample count                                                                                                                            |
+| pbrs-discounted-evidence-125                  | pbrs        | Verification requires local identity, continuity and discounted terminal boundary    | pbrs/test_pbrs.py::test_pbrs_canonical_discontinuous_potentials_report                              | Discontinuous potentials are not verified                                                                                                                                      |
+| statistics-proportional-histograms-126        | statistics  | Proportional histograms ignore sample count; moved mass yields positive KL/JS        | statistics/test_statistics.py::test_distribution_shift_proportional_histograms_ignore_sample_count  | KL and JS remain finite and non-negative                                                                                                                                       |
+| pbrs-near-bound-clamp-127                     | pbrs        | Relaxed near-bound clamps apply exact endpoints and retain all reasons               | pbrs/test_pbrs.py::test_validate_reward_parameters_records_near_bound_clamps_exactly                | Includes numeric-string coercion                                                                                                                                               |
+| pbrs-exit-mode-validation-128                 | pbrs        | Exit-potential choices are strict or canonicalized; direct calls fail safe           | pbrs/test_pbrs.py::test_invalid_exit_mode_warns_at_direct_and_simulation_boundaries                 | Direct calculation and simulation boundaries warn before fallback; PBRS calls suppress additives                                                                               |
+| cli-invalid-exit-mode-129                     | cli         | Invalid `--params exit_potential_mode` fails before artifacts                        | cli/test_cli_params_and_csv.py::test_invalid_exit_potential_mode_params_fails_before_artifacts      | Strict CLI validation                                                                                                                                                          |
+| cli-warning-header-recognition-130            | cli         | Warning counts accept only anchored Python warning header formats                    | cli/test_cli_params_and_csv.py::test_warning_header_positive_and_negative_formats                   | Covers POSIX, relative, synthetic and Windows source locations                                                                                                                 |
+| pbrs-invalid-mode-provenance-131              | pbrs        | Invalid imported exit-mode metadata cannot certify canonical invariance              | pbrs/test_pbrs.py::test_report_rejects_invalid_exit_mode_provenance                                 | Preserves the invalid raw value and reports effective additive settings as unknown                                                                                             |
+| pbrs-synthetic-fee-floor-132                  | pbrs        | High-fee entry loss remains in long/short synthetic PnL and exit rewards             | pbrs/test_pbrs.py::test_synthetic_fee_loss_extrema_match_retained_pnl                               | Direct and transformed trajectories                                                                                                                                            |
+| pbrs-synthetic-profitable-mark-133            | pbrs        | Favorable long/short marks remain profitable after high-fee synthetic transformation | pbrs/test_pbrs.py::test_synthetic_high_fee_winner_retains_profitable_mark                           | Covers immediate gains and recovery after an initial loss on the independent sampled market path                                                                               |
+| pbrs-synthetic-fee-boundary-134               | pbrs        | Fee-boundary rounding is accepted while materially extreme PnL is rejected           | pbrs/test_pbrs.py::test_synthetic_high_fee_short_boundary_rejects_real_excess                       | Non-unit entry price exposes floating-point roundoff                                                                                                                           |
+| pbrs-synthetic-latent-price-135               | pbrs        | Clipped candidate PnL does not erase the sampled market path for later holds         | pbrs/test_pbrs.py::test_unrealized_pnl_retains_sampled_market_path_after_candidate_cap              | Equal first retained marks from distinct market prices diverge after the same adverse return                                                                                   |
+| api-unmasked-invalid-actions-136              | api         | Unmasked invalid actions keep positions and receive penalties                        | api/test_api_helpers.py::test_unmasked_simulation_samples_invalid_actions_without_changing_position | Includes spot, futures and zero-penalty invalid actions                                                                                                                        |
+| api-terminal-invalid-exit-137                 | api         | Wrong-side terminal exits liquidate the held side                                    | api/test_api_helpers.py::test_invalid_terminal_exit_liquidates_the_held_position                    | Prevents dropped terminal trades                                                                                                                                               |
+| cli-stale-generated-artifacts-138             | cli         | Skipped analyses remove only reserved outputs from an earlier run                    | cli/test_cli_params_and_csv.py::test_skipped_analysis_removes_only_stale_generated_artifacts        | Preserves unrelated files                                                                                                                                                      |
+| helpers-nonfinite-episodes-139                | helpers     | Infinite real observations become missing without dropping rows                      | helpers/test_utilities.py::test_nonfinite_numeric_episodes_are_marked_missing                       | Retains transition multiplicity                                                                                                                                                |
+| statistics-finite-shift-140                   | statistics  | Finite observations remain comparable despite an infinite value                      | statistics/test_statistics.py::test_distribution_shift_uses_finite_observations                     | Rejects all-nonfinite features                                                                                                                                                 |
+| statistics-rank-direction-141                 | statistics  | Rank-biserial effect follows the named first-group advantage                         | statistics/test_statistics.py::test_pnl_rank_biserial_direction_matches_named_first_group           | Checks both directions                                                                                                                                                         |
+| statistics-bootstrap-count-142                | statistics  | Zero and negative resample counts fail for variable and constant data                | statistics/test_statistics.py::test_bootstrap_rejects_nonpositive_resample_count                    | Rejects missing bootstrap                                                                                                                                                      |
+| api-unmasked-sample-probabilities-144         | api         | Unmasked sample probabilities match marginal valid-action frequencies                | api/test_api_helpers.py::test_unmasked_sampling_probabilities_match_action_frequencies              | Spot/futures entries, long/short exits, and neutral probability                                                                                                                |
 
 ### Non-Owning Smoke / Reference Checks
 
index 3dde5ee9b5cabaa794681ec7408b51ed4c682d1a..cee90be2cccd048f11d708f76a15f281aaef4d15 100644 (file)
@@ -7,7 +7,6 @@ import unittest
 import numpy as np
 import pandas as pd
 import pytest
-
 from reward_space_analysis import PBRS_INVARIANCE_TOL, write_complete_statistical_analysis
 
 from ..constants import (
@@ -91,18 +90,6 @@ class TestReportFormatting(RewardSpaceTestBase):
         report_path = out_dir / "statistical_analysis.md"
         return report_path.read_text(encoding="utf-8")
 
-    def test_report_distinguishes_missing_real_episodes_from_unusable_observations(self):
-        synth_df = self.make_stats_df(n=SCENARIOS.SAMPLE_SIZE_TINY, seed=SEEDS.REPORT_FORMAT_1)
-        real_df = synth_df.copy()
-        real_df[["pnl", "trade_duration", "idle_duration"]] = np.inf
-        content = self._write_report(synth_df, real_df=real_df, skip_feature_analysis=True)
-        self.assertIn("_Not performed (no comparable finite observations)._", content)
-        self.assertIn(
-            "6. **Distribution Shift** - Not performed (no comparable finite observations)",
-            content,
-        )
-        self.assertNotIn("no real episodes provided", content)
-
     def test_distribution_shift_section_present_with_real_episodes(self):
         """Distribution Shift section renders metrics table when real episodes provided."""
         # Synthetic df (ensure >=10 non-NaN per feature)
index abdf3d4b695b355a93fee5dfe60a9e0081c773e6..59ad196d3942f63d64cbd6f7f1693728928964e5 100644 (file)
@@ -130,6 +130,36 @@ class ReviewContractsTest(unittest.TestCase):
                 strategy.config = {} if configured is None else {"leverage": configured}
                 self.assertEqual(strategy.leverage(**arguments), expected)
 
+    def test_strategy_leverage_warnings_follow_invalid_config_transitions(self):
+        """Warn once per invalid setting, including values below the leverage floor."""
+        strategy = RLAgentStrategy.__new__(RLAgentStrategy)
+        arguments = {
+            "pair": "BTC/USDT",
+            "current_time": dt(2026, 1, 1, tzinfo=timezone.utc),
+            "current_rate": 100.0,
+            "proposed_leverage": 2.0,
+            "max_leverage": 5.0,
+            "entry_tag": None,
+            "side": "long",
+        }
+        strategy.config = {"leverage": float("nan")}
+        with self.assertLogs(RLAgentStrategy.__module__, level="WARNING") as logs:
+            for _ in range(3):
+                self.assertEqual(strategy.leverage(**arguments), 2.0)
+            self.assertEqual(len(logs.records), 1)
+            strategy.config["leverage"] = float("inf")
+            self.assertEqual(strategy.leverage(**arguments), 2.0)
+            self.assertEqual(len(logs.records), 2)
+            strategy.config["leverage"] = 0.5
+            for _ in range(2):
+                self.assertEqual(strategy.leverage(**arguments), 1.0)
+            self.assertEqual(len(logs.records), 3)
+            strategy.config["leverage"] = 2.5
+            self.assertEqual(strategy.leverage(**arguments), 2.5)
+            strategy.config["leverage"] = float("nan")
+            self.assertEqual(strategy.leverage(**arguments), 2.0)
+            self.assertEqual(len(logs.records), 4)
+
     def test_training_preserves_raw_prices_and_returns_best_checkpoint(self):
         for drop in (False, True):
             with self.subTest(drop_ohlc_from_features=drop), tempfile.TemporaryDirectory() as temp:
index a1ab0d224803fe841c1df4490d5d3142d245ac6b..759e05cef2d51bdf25027efa516a356ee099a20d 100644 (file)
@@ -58,6 +58,7 @@ class RLAgentStrategy(IStrategy):
     _ACTION_EXIT_LONG: Final[int] = 2
     _ACTION_ENTER_SHORT: Final[int] = 3
     _ACTION_EXIT_SHORT: Final[int] = 4
+    _leverage_warning_state: tuple[str, str] | None = None
 
     @property
     def can_short(self) -> bool:
@@ -189,6 +190,7 @@ class RLAgentStrategy(IStrategy):
         :return: A leverage amount, which will be between 1.0 and max_leverage.
         """
         configured = self.config.get("leverage")
+        warning_kind: str | None = None
         if configured is None:
             requested = proposed_leverage
         else:
@@ -197,8 +199,20 @@ class RLAgentStrategy(IStrategy):
             except (TypeError, ValueError, OverflowError):
                 requested = float("nan")
             if isinstance(configured, bool) or not np.isfinite(requested):
-                logger.warning("Invalid leverage value %r; using proposed leverage", configured)
+                warning_kind = "invalid"
                 requested = proposed_leverage
+            elif requested < 1.0:
+                warning_kind = "below_minimum"
+
+        warning_state = (warning_kind, repr(configured)) if warning_kind else None
+        if warning_state is not None and warning_state != self._leverage_warning_state:
+            if warning_kind == "invalid":
+                logger.warning("Invalid leverage value %r; using proposed leverage", configured)
+            else:
+                logger.warning(
+                    "Invalid leverage value %r; must be >= 1.0, clamping to 1.0", configured
+                )
+        self._leverage_warning_state = warning_state
         return float(max(1.0, min(requested, max_leverage)))
 
     def is_short_allowed(self) -> bool: