From 2436a5553afa33c1b225baeb465eef97ed7ea339 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Thu, 24 Sep 2026 01:20:30 +0200 Subject: [PATCH] fix(reforcexy): clarify diagnostics and deduplicate leverage warnings --- ReforceXY/.basedpyright/diagnostics.json | 84 ++++++++--------- ReforceXY/reward_space_analysis/README.md | 5 +- .../reward_space_analysis.py | 28 ++++-- .../reward_space_analysis/tests/README.md | 93 +++++++++---------- .../integration/test_report_formatting.py | 13 --- ReforceXY/tests/test_review_contracts.py | 30 ++++++ .../user_data/strategies/RLAgentStrategy.py | 16 +++- 7 files changed, 158 insertions(+), 111 deletions(-) diff --git a/ReforceXY/.basedpyright/diagnostics.json b/ReforceXY/.basedpyright/diagnostics.json index 61c476f..1bacced 100644 --- a/ReforceXY/.basedpyright/diagnostics.json +++ b/ReforceXY/.basedpyright/diagnostics.json @@ -33,173 +33,173 @@ }, { "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, @@ -1143,43 +1143,43 @@ }, { "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, diff --git a/ReforceXY/reward_space_analysis/README.md b/ReforceXY/reward_space_analysis/README.md index 0df5b6f..5000b4e 100644 --- a/ReforceXY/reward_space_analysis/README.md +++ b/ReforceXY/reward_space_analysis/README.md @@ -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. --- diff --git a/ReforceXY/reward_space_analysis/reward_space_analysis.py b/ReforceXY/reward_space_analysis/reward_space_analysis.py index ed6b312..c4f6854 100644 --- a/ReforceXY/reward_space_analysis/reward_space_analysis.py +++ b/ReforceXY/reward_space_analysis/reward_space_analysis.py @@ -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 diff --git a/ReforceXY/reward_space_analysis/tests/README.md b/ReforceXY/reward_space_analysis/tests/README.md index 560ec9d..4b0bc00 100644 --- a/ReforceXY/reward_space_analysis/tests/README.md +++ b/ReforceXY/reward_space_analysis/tests/README.md @@ -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 diff --git a/ReforceXY/reward_space_analysis/tests/integration/test_report_formatting.py b/ReforceXY/reward_space_analysis/tests/integration/test_report_formatting.py index 3dde5ee..cee90be 100644 --- a/ReforceXY/reward_space_analysis/tests/integration/test_report_formatting.py +++ b/ReforceXY/reward_space_analysis/tests/integration/test_report_formatting.py @@ -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) diff --git a/ReforceXY/tests/test_review_contracts.py b/ReforceXY/tests/test_review_contracts.py index abdf3d4..59ad196 100644 --- a/ReforceXY/tests/test_review_contracts.py +++ b/ReforceXY/tests/test_review_contracts.py @@ -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: diff --git a/ReforceXY/user_data/strategies/RLAgentStrategy.py b/ReforceXY/user_data/strategies/RLAgentStrategy.py index a1ab0d2..759e05c 100644 --- a/ReforceXY/user_data/strategies/RLAgentStrategy.py +++ b/ReforceXY/user_data/strategies/RLAgentStrategy.py @@ -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: -- 2.53.0