},
{
"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,
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.
---
)
# 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)]
& 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)]
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,
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)
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())
"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
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
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 (
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)
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:
_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:
: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:
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: