},
{
"endCharacter": 5,
- "endLine": 2156,
+ "endLine": 2194,
"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": 2151
+ "startLine": 2189
},
{
"endCharacter": 21,
- "endLine": 2153,
+ "endLine": 2191,
"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": 2153
+ "startLine": 2191
},
{
"endCharacter": 5,
- "endLine": 2178,
+ "endLine": 2216,
"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": 2174
+ "startLine": 2212
},
{
"endCharacter": 43,
- "endLine": 2175,
+ "endLine": 2213,
"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": 2175
+ "startLine": 2213
},
{
"endCharacter": 42,
- "endLine": 2175,
+ "endLine": 2213,
"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": 2175
+ "startLine": 2213
},
{
"endCharacter": 60,
- "endLine": 2435,
+ "endLine": 2473,
"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": 2435
+ "startLine": 2473
},
{
"endCharacter": 58,
- "endLine": 2436,
+ "endLine": 2474,
"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": 2436
+ "startLine": 2474
},
{
"endCharacter": 88,
- "endLine": 2455,
+ "endLine": 2493,
"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": 2455
+ "startLine": 2493
},
{
"endCharacter": 88,
- "endLine": 2455,
+ "endLine": 2493,
"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": 2455
+ "startLine": 2493
},
{
"endCharacter": 17,
- "endLine": 2464,
+ "endLine": 2502,
"file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
"message": "Object of type \"None\" cannot be called",
"rule": "reportOptionalCall",
"severity": "error",
"startCharacter": 28,
- "startLine": 2458
+ "startLine": 2496
},
{
"endCharacter": 29,
- "endLine": 3063,
+ "endLine": 3101,
"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": 3063
+ "startLine": 3101
},
{
"endCharacter": 28,
- "endLine": 3063,
+ "endLine": 3101,
"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": 3063
+ "startLine": 3101
},
{
"endCharacter": 65,
- "endLine": 3072,
+ "endLine": 3110,
"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": 3072
+ "startLine": 3110
},
{
"endCharacter": 65,
- "endLine": 3072,
+ "endLine": 3110,
"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": 3072
+ "startLine": 3110
},
{
"endCharacter": 5,
- "endLine": 3972,
+ "endLine": 4022,
"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": 3968
+ "startLine": 4018
},
{
"endCharacter": 43,
- "endLine": 3969,
+ "endLine": 4019,
"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": 3969
+ "startLine": 4019
},
{
"endCharacter": 42,
- "endLine": 3969,
+ "endLine": 4019,
"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": 3969
+ "startLine": 4019
},
{
"endCharacter": 22,
counts for RandomForest and permutation importance (-1 = all cores).
Programmatic reward parameters accept Python and NumPy real integer/floating
-scalars. Strict validation checks a numeric gamma in its native precision before
-normalizing it. Analysis-specific numeric string/bool coercion is retained; it
-does not replace the stricter runtime configuration contract. Direct gamma
-calculation falls back to 0.95 for invalid/non-finite input and clamps finite
-out-of-range values; relaxed parameter validation records its resets/clamps.
+scalars. Native duration scalars are checked against their bounds before direct
+conversion to Python integers, without binary64 narrowing or a fixed-width cap.
+Adjustment metadata for finite native durations records the final normalized
+integer. Strict validation checks a numeric gamma in its native precision before
+normalizing it.
+Analysis-specific numeric string/bool coercion is retained; it does not replace
+the stricter runtime configuration contract. Direct gamma calculation falls back
+to 0.95 for invalid/non-finite input and clamps finite out-of-range values before
+narrowing, including extended-precision scalars. Relaxed parameter validation
+records its resets/clamps and also checks finite native gamma bounds before
+narrowing.
For a trained or resumed learner, supply its effective discount as
`potential_gamma`, not a stale reward/config gamma.
Returns a sanitized copy plus adjustments mapping (param -> original/adjusted/reason).
Behavior:
- Boolean-like keys are coerced to bool.
+ - Native duration bounds are checked before conversion directly to Python int.
+ - Finite out-of-range gamma is clamped before float conversion in relaxed mode.
- Numeric-bounded keys are coerced to float when provided as str/bool/None.
* In strict mode: raise on non-numeric or out-of-bounds.
* In relaxed mode: fallback to min bound or 0.0 with adjustment reason.
and not bounds["min"] <= original_val <= bounds["max"]
):
raise ValueError(f"Param: '{key}'={original_val!r} outside [0, 1]")
+ # Duration conversion and finite gamma clamps must precede binary64 narrowing.
+ native_duration = key in ("max_trade_duration_candles", "max_idle_duration_candles")
+ if (native_duration or key == "potential_gamma") and isinstance(
+ original_val, (int, float, np.integer, np.floating)
+ ):
+ native_finite = isinstance(original_val, (int, np.integer)) or np.isfinite(original_val)
+ if native_finite and (
+ native_duration or not bounds["min"] <= original_val <= bounds["max"]
+ ):
+ adjusted_native = original_val
+ reason_parts = []
+ if "min" in bounds and adjusted_native < bounds["min"]:
+ if strict:
+ raise ValueError(f"Param: '{key}'={original_val} below min {bounds['min']}")
+ adjusted_native = bounds["min"]
+ reason_parts.append(f"min={bounds['min']}")
+ if "max" in bounds and adjusted_native > bounds["max"]:
+ if strict:
+ raise ValueError(f"Param: '{key}'={original_val} above max {bounds['max']}")
+ adjusted_native = bounds["max"]
+ reason_parts.append(f"max={bounds['max']}")
+ adjusted_native = (
+ int(adjusted_native) if native_duration else float(adjusted_native)
+ )
+ if not isinstance(original_val, (int, float)):
+ reason_parts.insert(0, "numeric_coerce")
+ sanitized[key] = adjusted_native
+ if reason_parts:
+ adjustments[key] = {
+ "original": original_val,
+ "adjusted": adjusted_native,
+ "reason": ",".join(reason_parts),
+ "validation_mode": "strict" if strict else "relaxed",
+ }
+ continue
+
# Robust coercion to float using helper (handles None/str/bool/non-finite)
coerced_val = _get_float_param({key: original_val}, key, np.nan)
}
for key in ("max_trade_duration_candles", "max_idle_duration_candles"):
if key in sanitized:
- sanitized[key] = int(_get_float_param(sanitized, key))
+ sanitized[key] = _get_int_param(sanitized, key)
return sanitized, adjustments
- Guarantee returned float ∈ [0,1].
"""
raw_gamma = params.get("potential_gamma")
+ bounds = _PARAMETER_BOUNDS["potential_gamma"]
+ if isinstance(raw_gamma, (int, float, np.integer, np.floating)):
+ native_finite = isinstance(raw_gamma, (int, np.integer)) or np.isfinite(raw_gamma)
+ if native_finite and not bounds["min"] <= raw_gamma <= bounds["max"]:
+ gamma = float(bounds["min"] if raw_gamma < bounds["min"] else bounds["max"])
+ warnings.warn(
+ f"PBRS: potential_gamma={raw_gamma} outside [0,1]; clamped to {gamma}",
+ RewardDiagnosticsWarning,
+ stacklevel=2,
+ )
+ return gamma
+
gamma = _get_float_param(params, "potential_gamma", np.nan)
if not np.isfinite(gamma):
if "potential_gamma" in params:
| 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 |
| cli-effective-reward-boundaries-145 | cli | Effective profit-target fallback and explicit zero idle denominator reach artifacts | cli/test_cli_params_and_csv.py::test_zero_target_and_idle_cap_reach_rewards_and_manifest | Metadata and near-target market coverage agree with runtime boundaries |
+| pbrs-native-integer-durations-146 | pbrs | Native integer durations and adjustment metadata retain exact values | pbrs/test_pbrs.py::test_native_integer_durations_preserve_exact_values | Python, signed NumPy and unsigned NumPy integers; strict and relaxed |
+| pbrs-native-duration-bounds-147 | pbrs | Floating duration bounds are checked before truncation without narrowing | pbrs/test_pbrs.py::test_native_duration_bounds_precede_truncation | Extended-precision cases run when supported by the platform |
+| pbrs-native-gamma-reward-148 | pbrs | Finite out-of-range native gamma clamps reach the public reward | pbrs/test_pbrs.py::test_native_finite_gamma_clamps_the_public_reward | Extended-range cases run when supported by the platform |
+| pbrs-relaxed-native-gamma-149 | pbrs | Relaxed native gamma clamps reach rewards while strict validation rejects | pbrs/test_pbrs.py::test_relaxed_native_gamma_clamps_before_reward_calculation | Preserves direct, strict and relaxed boundary contracts |
### Non-Owning Smoke / Reference Checks
},
)
+ def test_native_integer_durations_preserve_exact_values(self):
+ """Validation preserves exact duration values and adjustment metadata."""
+ values = (
+ 2**53 + 1,
+ np.int64(2**53 + 1),
+ np.int64(2**63 - 1),
+ np.uint64(2**64 - 1),
+ 2**200 + 1,
+ )
+ for key in ("max_trade_duration_candles", "max_idle_duration_candles"):
+ for strict in (True, False):
+ for value in values:
+ with self.subTest(key=key, strict=strict, value=value):
+ normalized, adjustments = validate_reward_parameters(
+ {key: value}, strict=strict
+ )
+ self.assertEqual(normalized[key], int(value))
+ if isinstance(value, np.integer):
+ self.assertEqual(adjustments[key]["adjusted"], int(value))
+
+ def test_native_duration_bounds_precede_truncation(self):
+ """Native floating durations truncate only after their bounds are checked."""
+ values = [np.float32(3.75), np.float64(3.75)]
+ if np.finfo(np.longdouble).nmant > np.finfo(np.float64).nmant:
+ values.extend(
+ [np.longdouble("9007199254740993.0"), np.longdouble("9007199254740993.5")]
+ )
+ for key, below_minimum, minimum in (
+ ("max_trade_duration_candles", np.longdouble("0.5"), 1),
+ ("max_idle_duration_candles", np.longdouble("-0.5"), 0),
+ ):
+ for value in values:
+ for strict in (True, False):
+ with self.subTest(key=key, strict=strict, value=value):
+ normalized, _ = validate_reward_parameters({key: value}, strict=strict)
+ self.assertEqual(normalized[key], int(value))
+ with self.subTest(key=key, below_minimum=below_minimum):
+ with self.assertRaises(ValueError):
+ validate_reward_parameters({key: below_minimum}, strict=True)
+ normalized, _ = validate_reward_parameters({key: below_minimum}, strict=False)
+ self.assertEqual(normalized[key], minimum)
+
+ def test_native_finite_gamma_clamps_the_public_reward(self):
+ """Finite native discounts outside the domain use endpoints, not a fallback."""
+ values = [(np.float64(2.0), 0.0), (np.float64(-1.0), -0.025)]
+ if np.finfo(np.longdouble).maxexp > np.finfo(np.float64).maxexp:
+ values.extend([(np.longdouble("1e400"), 0.0), (np.longdouble("-1e400"), -0.025)])
+ context = reward_space_analysis.RewardContext(
+ 0.0, 0, 1, 0.0, 0.0, Positions.Neutral, Actions.Neutral
+ )
+ for gamma, expected_shaping in values:
+ with self.subTest(gamma=gamma):
+ params = self.base_params(
+ potential_gamma=gamma,
+ hold_potential_enabled=True,
+ exit_potential_mode="retain_previous",
+ idle_penalty_ratio=0.0,
+ )
+ with self.assertWarns(reward_space_analysis.RewardDiagnosticsWarning):
+ reward = reward_space_analysis.calculate_reward(
+ context,
+ params,
+ 100.0,
+ 0.03,
+ 2.0,
+ short_allowed=True,
+ action_masking=True,
+ prev_potential=0.025,
+ )
+ self.assertEqual(reward.reward_shaping, expected_shaping)
+ self.assertEqual(reward.total, expected_shaping)
+
+ def test_relaxed_native_gamma_clamps_before_reward_calculation(self):
+ """Relaxed finite-native clamps reach the reward; strict validation still rejects."""
+ values = [(np.float64(2.0), 1.0), (np.float64(-1.0), 0.0)]
+ if np.finfo(np.longdouble).maxexp > np.finfo(np.float64).maxexp:
+ values.extend([(np.longdouble("1e400"), 1.0), (np.longdouble("-1e400"), 0.0)])
+ context = reward_space_analysis.RewardContext(
+ 0.0, 0, 1, 0.0, 0.0, Positions.Neutral, Actions.Neutral
+ )
+ for gamma, expected_gamma in values:
+ with self.subTest(gamma=gamma):
+ with self.assertRaises(ValueError):
+ validate_reward_parameters({"potential_gamma": gamma}, strict=True)
+ normalized, adjustments = validate_reward_parameters(
+ {"potential_gamma": gamma}, strict=False
+ )
+ self.assertEqual(normalized["potential_gamma"], expected_gamma)
+ self.assertEqual(adjustments["potential_gamma"]["adjusted"], expected_gamma)
+ params = self.base_params(
+ **normalized,
+ hold_potential_enabled=True,
+ exit_potential_mode="retain_previous",
+ idle_penalty_ratio=0.0,
+ )
+ reward = reward_space_analysis.calculate_reward(
+ context,
+ params,
+ 100.0,
+ 0.03,
+ 2.0,
+ short_allowed=True,
+ action_masking=True,
+ prev_potential=0.025,
+ )
+ self.assertEqual(reward.reward_shaping, expected_gamma * 0.025 - 0.025)
+
def test_validate_reward_parameters_records_near_bound_clamps_exactly(self):
"""Relaxed validation applies near-bound clamps without approximate-equality suppression."""
cases = (