"diagnostics": [
{
"endCharacter": 58,
- "endLine": 323,
+ "endLine": 324,
"file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
"message": "Type \"RewardParamValue\" is not assignable to declared type \"bool | None\"\n Type \"RewardParamValue\" is not assignable to type \"bool | None\"\n Type \"float\" is not assignable to type \"bool | None\"\n \"float\" is not assignable to \"bool\"\n \"float\" is not assignable to \"None\"",
"rule": "reportAssignmentType",
"severity": "error",
"startCharacter": 18,
- "startLine": 323
+ "startLine": 324
},
{
"endCharacter": 92,
- "endLine": 581,
+ "endLine": 597,
"file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
"message": "Argument of type \"RewardParamValue\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n Type \"RewardParamValue\" is not assignable to type \"ConvertibleToInt\"\n Type \"None\" is not assignable to type \"ConvertibleToInt\"\n \"None\" is not assignable to \"str\"\n \"None\" is incompatible with protocol \"Buffer\"\n \"__buffer__\" is not present\n \"None\" is incompatible with protocol \"SupportsInt\"\n \"__int__\" is not present\n \"None\" is incompatible with protocol \"SupportsIndex\"",
"rule": "reportArgumentType",
"severity": "error",
"startCharacter": 22,
- "startLine": 581
+ "startLine": 597
},
{
"endCharacter": 92,
- "endLine": 581,
+ "endLine": 597,
"file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
"message": "Argument of type \"RewardParamValue\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n Type \"RewardParamValue\" is not assignable to type \"ConvertibleToInt\"\n Type \"None\" is not assignable to type \"ConvertibleToInt\"\n \"None\" is not assignable to \"str\"\n \"None\" is incompatible with protocol \"Buffer\"\n \"__buffer__\" is not present\n \"None\" is incompatible with protocol \"SupportsInt\"\n \"__int__\" is not present\n \"None\" is incompatible with protocol \"SupportsIndex\"\n ...",
"rule": "reportArgumentType",
"severity": "error",
"startCharacter": 22,
- "startLine": 581
+ "startLine": 597
},
{
"endCharacter": 5,
- "endLine": 2194,
+ "endLine": 2210,
"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": 2189
+ "startLine": 2205
},
{
"endCharacter": 21,
- "endLine": 2191,
+ "endLine": 2207,
"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": 2191
+ "startLine": 2207
},
{
"endCharacter": 5,
- "endLine": 2216,
+ "endLine": 2273,
"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": 2212
+ "startLine": 2269
},
{
"endCharacter": 43,
- "endLine": 2213,
+ "endLine": 2270,
"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": 2213
+ "startLine": 2270
},
{
"endCharacter": 42,
- "endLine": 2213,
+ "endLine": 2270,
"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": 2213
+ "startLine": 2270
},
{
"endCharacter": 60,
- "endLine": 2473,
+ "endLine": 2532,
"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": 2473
+ "startLine": 2532
},
{
"endCharacter": 58,
- "endLine": 2474,
+ "endLine": 2533,
"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": 2474
+ "startLine": 2533
},
{
"endCharacter": 88,
- "endLine": 2493,
+ "endLine": 2552,
"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": 2493
+ "startLine": 2552
},
{
"endCharacter": 88,
- "endLine": 2493,
+ "endLine": 2552,
"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": 2493
+ "startLine": 2552
},
{
"endCharacter": 17,
- "endLine": 2502,
+ "endLine": 2561,
"file": "ReforceXY/reward_space_analysis/reward_space_analysis.py",
"message": "Object of type \"None\" cannot be called",
"rule": "reportOptionalCall",
"severity": "error",
"startCharacter": 28,
- "startLine": 2496
+ "startLine": 2555
},
{
"endCharacter": 29,
- "endLine": 3101,
+ "endLine": 3160,
"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": 3101
+ "startLine": 3160
},
{
"endCharacter": 28,
- "endLine": 3101,
+ "endLine": 3160,
"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": 3101
+ "startLine": 3160
},
{
"endCharacter": 65,
- "endLine": 3110,
+ "endLine": 3169,
"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": 3110
+ "startLine": 3169
},
{
"endCharacter": 65,
- "endLine": 3110,
+ "endLine": 3169,
"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": 3110
+ "startLine": 3169
},
{
"endCharacter": 5,
- "endLine": 4022,
+ "endLine": 4081,
"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": 4018
+ "startLine": 4077
},
{
"endCharacter": 43,
- "endLine": 4019,
+ "endLine": 4078,
"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": 4019
+ "startLine": 4078
},
{
"endCharacter": 42,
- "endLine": 4019,
+ "endLine": 4078,
"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": 4019
+ "startLine": 4078
},
{
"endCharacter": 22,
Adjustment metadata for finite native durations records the final normalized
integer. Strict validation checks a numeric gamma in its native precision before
normalizing it.
+Simulation consumes extended integer durations without a binary64 cap. Ordinary
+fractional duration caps retain float-product truncation; extended caps use the
+supplied ratio's exact rational value. Duration relationship tables keep candle
+units when their float bounds and labels are safe. At larger scales, the same
+12 right-closed uniform bins use exact assignment and explicitly declared
+`duration / max_trade_duration_candles` coordinates; the first bin includes zero.
+Report configuration retains exact integer durations without changing Python's
+integer-string digit limit. Non-finite explicit Python/NumPy trade-duration
+overrides in the idle-cap helper use the configured duration, then the canonical
+default; an explicit idle threshold retains precedence.
+
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
import pickle
import random
import warnings
+from decimal import Decimal
from enum import Enum, IntEnum
from pathlib import Path
from typing import TYPE_CHECKING, Any, Final, Literal
return trade_duration / max(1, max_trade_duration_candles)
+def _scale_duration(duration: int, ratio: float) -> int:
+ """Keep ordinary float truncation; scale extended integers without narrowing."""
+ if duration.bit_length() <= 53:
+ scaled = duration * ratio
+ if math.isfinite(scaled):
+ return int(scaled)
+ numerator, denominator = ratio.as_integer_ratio()
+ product = duration * numerator
+ return product // denominator if product >= 0 else -(-product // denominator)
+
+
def _resolve_pnl_target(profit_aim: float, risk_reward_ratio: float) -> float:
"""Resolve the effective target used by runtime rewards and analysis diagnostics."""
target = float(profit_aim * risk_reward_ratio)
def get_max_idle_duration_candles(
params: RewardParams,
*,
- max_trade_duration_candles: int | None = None,
+ max_trade_duration_candles: float | np.integer[Any] | np.floating[Any] | None = None,
) -> int:
mtd = (
- int(max_trade_duration_candles)
+ _get_int_param(
+ {"max_trade_duration_candles": max_trade_duration_candles},
+ "max_trade_duration_candles",
+ 0,
+ )
if isinstance(max_trade_duration_candles, (int, float, np.integer, np.floating))
- else None
+ else 0
)
- if mtd is None or mtd <= 0:
+ if mtd <= 0:
mtd = _get_int_param(params, "max_trade_duration_candles")
if mtd <= 0:
mtd = int(DEFAULT_MODEL_REWARD_PARAMETERS.get("max_trade_duration_candles", 128))
duration_ratio = _compute_duration_ratio(trade_duration, max_trade_duration_candles)
- base_exit_prob = 1.0 / max(1, int(max_trade_duration_candles))
+ base_exit_prob = 1 / max(1, int(max_trade_duration_candles))
base_exit_prob = float(
np.clip(base_exit_prob, _SAMPLE_EXIT_PROBABILITY_MIN, _SAMPLE_EXIT_PROBABILITY_MAX)
)
max_idle_duration_candles = get_max_idle_duration_candles(
params, max_trade_duration_candles=max_trade_duration_candles
)
- max_trade_duration_cap = int(max_trade_duration_candles * max_duration_ratio)
+ max_trade_duration_cap = _scale_duration(max_trade_duration_candles, max_duration_ratio)
samples: list[dict[str, float | None]] = []
prev_potential: float = 0.0
include_lowest=True,
duplicates="drop",
)
+ return _aggregate_binned_stats(df, target, categories)
+
+
+def _aggregate_binned_stats(
+ df: pd.DataFrame, target: str, categories: pd.Series[Any] | pd.Categorical[str]
+) -> pd.DataFrame:
+ """Aggregate every bin, including empty bins, without changing reward observations."""
aggregated = (
pd.DataFrame({"bin": categories, target: df[target]})
.dropna(subset=["bin"])
return aggregated
+# Three-duration endpoints, pandas' precision=3 rounding, and a factor-two margin.
+_DURATION_BIN_FLOAT_MAX: Final[int] = int(np.finfo(float).max) // 6000
+
+
+def _duration_binned_stats(
+ df: pd.DataFrame, column: str, target: str, max_duration: int
+) -> pd.DataFrame:
+ """Use exact category assignment when candle-valued float bins cannot be rendered."""
+ if max_duration <= _DURATION_BIN_FLOAT_MAX:
+ return _binned_stats(df, column, target, np.linspace(0, max_duration * 3.0, 13))
+
+ def bin_code(value: Any) -> int:
+ if pd.isna(value):
+ return -1
+ if isinstance(value, (int, np.integer)):
+ numerator, denominator = int(value), 1
+ elif not np.isfinite(value):
+ return 11 if value > 0 else 0
+ else:
+ numerator, denominator = value.as_integer_ratio()
+ # ceil(4 * value / max_duration) - 1 implements right-closed bins exactly.
+ return min(11, max(0, (4 * numerator - 1) // (max_duration * denominator)))
+
+ labels = [f"{'[' if i == 0 else '('}{i / 4:g}, {(i + 1) / 4:g}]" for i in range(12)]
+ categories = pd.Categorical.from_codes(
+ np.fromiter((bin_code(value) for value in df[column]), dtype=np.int8, count=len(df)),
+ categories=pd.Index(labels),
+ ordered=True,
+ )
+ aggregated = _aggregate_binned_stats(df, target, categories)
+ aggregated.index.name = "duration / max_trade_duration_candles"
+ return aggregated
+
+
def _compute_relationship_stats(df: pd.DataFrame) -> dict[str, Any]:
"""Return binned stats dict for idle, trade duration and pnl (uniform bins).
else {}
)
max_trade_duration_candles = _get_int_param(reward_params, "max_trade_duration_candles")
- idle_bins = np.linspace(0, max_trade_duration_candles * 3.0, 13)
- trade_bins = np.linspace(0, max_trade_duration_candles * 3.0, 13)
exit_pnl = df.get("exit_pnl", df["pnl"]).fillna(df["pnl"])
pnl_min = float(exit_pnl.min())
pnl_max = float(exit_pnl.max())
pnl_max = pnl_min + 1e-6
pnl_bins = np.linspace(pnl_min, pnl_max, 13)
- idle_stats = _binned_stats(df, "idle_duration", "reward_idle", idle_bins)
- hold_stats = _binned_stats(df, "trade_duration", "reward_hold", trade_bins)
+ idle_stats = _duration_binned_stats(
+ df, "idle_duration", "reward_idle", max_trade_duration_candles
+ )
+ hold_stats = _duration_binned_stats(
+ df, "trade_duration", "reward_hold", max_trade_duration_candles
+ )
exit_stats = _binned_stats(df.assign(exit_pnl=exit_pnl), "exit_pnl", "reward_exit", pnl_bins)
idle_stats = idle_stats.round(6)
def _fmt_val(v: Any, ndigits: int = 6) -> str:
try:
if isinstance(v, numbers.Integral):
- return f"{int(v)}"
+ return str(Decimal(int(v)))
elif isinstance(v, numbers.Real):
fv = float(v)
if math.isnan(fv):
f.write(f"| exit_potential_mode | {exit_mode} |\n")
f.write(f"| potential_gamma | {potential_gamma} |\n")
# Additional configuration details
- f.write(f"| max_trade_duration_candles | {max_trade_duration_candles} |\n")
+ f.write(f"| max_trade_duration_candles | {Decimal(max_trade_duration_candles)} |\n")
max_idle_duration_candles = get_max_idle_duration_candles(
reward_params, max_trade_duration_candles=max_trade_duration_candles
)
- f.write(f"| max_idle_duration_candles | {max_idle_duration_candles} |\n")
+ f.write(f"| max_idle_duration_candles | {Decimal(max_idle_duration_candles)} |\n")
f.write(f"| strict_diagnostics | {strict_diagnostics} |\n")
f.write(f"| skip_feature_analysis | {skip_feature_analysis} |\n")
f.write(f"| skip_partial_dependence | {skip_partial_dependence} |\n")
continue # already printed explicitly
try:
if k in reward_params and reward_params[k] != default_v:
- overrides_pairs.append(f"{k}={reward_params[k]}")
+ value = reward_params[k]
+ rendered = (
+ str(Decimal(int(value)))
+ if isinstance(value, numbers.Integral) and not isinstance(value, bool)
+ else str(value)
+ )
+ overrides_pairs.append(f"{k}={rendered}")
except Exception:
continue
if overrides_pairs:
| pbrs-native-duration-bounds-151 | 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 |
+| api-native-duration-consumers-152 | api | Validated extended native durations reach real trajectories and finite rewards | api/test_api_helpers.py::test_simulation_consumes_extended_native_duration_without_narrowing | Includes the actual exit hazard and terminal state |
+| api-fractional-duration-cap-153 | api | Ordinary fractional caps retain truncation and rounded boundaries | api/test_api_helpers.py::test_simulation_preserves_small_fractional_duration_caps | Includes zero cap and one-third / one-seventh ratios |
+| api-nonfinite-duration-fallback-154 | api | Explicit non-finite native durations preserve configured/default idle-cap precedence | api/test_api_helpers.py::test_nonfinite_explicit_duration_uses_configured_then_default_idle_cap | Python and NumPy NaN and both infinities |
+| api-unbounded-duration-report-155 | api | Public reports retain exact duration configuration beyond integer-string digit limits | api/test_api_helpers.py::test_public_report_preserves_unbounded_duration_configuration | Leaves interpreter globals and boolean override semantics unchanged |
+| statistics-extended-duration-mass-156 | statistics | Large-duration bins retain all observations, reward means and declared ratio units | statistics/test_statistics.py::test_extended_duration_relationship_bins_preserve_mass_and_reward_means | Actual simulator output |
+| statistics-exact-duration-boundary-157 | statistics | Adjacent integer observations at extended right-closed boundaries stay distinguishable | statistics/test_statistics.py::test_extended_duration_bins_distinguish_exact_right_closed_boundaries | Includes outside-range clipping |
+| statistics-candle-duration-units-158 | statistics | Ordinary duration relationship tables retain candle-valued boundaries | statistics/test_statistics.py::test_ordinary_duration_relationship_bins_retain_candle_units | Unchanged ordinary output scale |
+| statistics-nullable-duration-bins-159 | statistics | Nullable numeric duration observations remain excluded from aggregates at extended scales | statistics/test_statistics.py::test_extended_duration_bins_preserve_nullable_observation_semantics | Counts and reward means use only nonmissing observations |
+
### Non-Owning Smoke / Reference Checks
#!/usr/bin/env python3
"""Tests for public API and helper functions."""
+import ast
import math
import random
+import sys
import tempfile
import unittest
+from decimal import Decimal
from pathlib import Path
from typing import Any, cast
from unittest import mock
_get_str_param,
_sample_action,
build_argument_parser,
+ get_max_idle_duration_candles,
parse_overrides,
+ validate_reward_parameters,
write_complete_statistical_analysis,
)
class TestAPIAndHelpers(RewardSpaceTestBase):
"""Public API + helper utility tests."""
+ def test_nonfinite_explicit_duration_uses_configured_then_default_idle_cap(self):
+ """Non-finite native overrides must not bypass configured/default precedence."""
+ for scalar in (float, np.float32, np.float64, np.longdouble):
+ for raw in ("nan", "inf", "-inf"):
+ value = scalar(raw)
+ for params, expected in (
+ ({"max_trade_duration_candles": 7}, 28),
+ ({}, 512),
+ ({"max_trade_duration_candles": 0}, 512),
+ ({"max_trade_duration_candles": 7, "max_idle_duration_candles": 13}, 13),
+ ):
+ with self.subTest(scalar=scalar, raw=raw, params=params):
+ self.assertEqual(
+ get_max_idle_duration_candles(params, max_trade_duration_candles=value),
+ expected,
+ )
+ self.assertEqual(
+ get_max_idle_duration_candles(
+ {"max_trade_duration_candles": 7}, max_trade_duration_candles=np.float32(3.75)
+ ),
+ 12,
+ )
+ self.assertEqual(
+ get_max_idle_duration_candles({}, max_trade_duration_candles=10**400),
+ 4 * 10**400,
+ )
+
+ def test_simulation_consumes_extended_native_duration_without_narrowing(self):
+ """Validated large durations remain usable by trajectory and hazard consumers."""
+ values = [10**308, 10**400]
+ if np.finfo(np.longdouble).maxexp > 1024:
+ values.append(np.longdouble("1e400"))
+ for value in values:
+ with self.subTest(value_type=type(value), magnitude=int(value).bit_length()):
+ params, _ = validate_reward_parameters(
+ self.base_params(max_trade_duration_candles=value),
+ strict=True,
+ )
+ df = simulate_samples_with_defaults(
+ params,
+ num_samples=32,
+ seed=42,
+ trading_mode="spot",
+ )
+ self.assertEqual(
+ df.attrs["reward_params"]["max_trade_duration_candles"], int(value)
+ )
+ self.assertTrue(
+ np.isfinite(df[["reward", "duration_ratio", "idle_ratio"]]).all().all()
+ )
+ held = df[df["position"] == Positions.Long.value]
+ self.assertGreater(held["trade_duration"].max(), 0)
+ self.assertTrue(held["sample_exit_prob"].eq(0.002).all())
+ self.assertTrue(df.iloc[-1]["terminated"])
+ self.assertEqual(df.iloc[-1]["next_position"], Positions.Neutral.value)
+
+ def test_simulation_preserves_small_fractional_duration_caps(self):
+ """Keep ordinary float-product truncation, including zero and rounded boundaries."""
+ for duration, ratio, expected in ((1, 0.5, 0), (3, 1 / 3, 1), (7, 1 / 7, 1), (10, 0.3, 3)):
+ with self.subTest(duration=duration, ratio=ratio):
+ df = simulate_samples_with_defaults(
+ self.base_params(max_trade_duration_candles=duration),
+ num_samples=8,
+ seed=42,
+ trading_mode="spot",
+ max_duration_ratio=ratio,
+ )
+ self.assertEqual(df["next_trade_duration"].max(), expected)
+
+ def test_public_report_preserves_unbounded_duration_configuration(self):
+ """The real report retains exact integer configuration beyond Python digit limits."""
+ duration = 10**4900
+ digit_limit = sys.get_int_max_str_digits()
+ df = simulate_samples_with_defaults(
+ self.base_params(max_trade_duration_candles=duration, check_invariants=False),
+ num_samples=32,
+ seed=42,
+ trading_mode="spot",
+ )
+ write_complete_statistical_analysis(
+ df,
+ self.output_path,
+ PARAMS.PROFIT_AIM,
+ PARAMS.RISK_REWARD_RATIO,
+ 42,
+ skip_feature_analysis=True,
+ )
+ report = (self.output_path / "statistical_analysis.md").read_text()
+ configuration = {
+ fields[1].strip(): fields[2].strip()
+ for line in report.splitlines()
+ if line.startswith("| max_")
+ for fields in [line.split("|")]
+ }
+ self.assertEqual(Decimal(configuration["max_trade_duration_candles"]), Decimal(duration))
+ self.assertEqual(Decimal(configuration["max_idle_duration_candles"]), Decimal(4 * duration))
+ self.assertEqual(sys.get_int_max_str_digits(), digit_limit)
+ override_row = next(
+ line for line in report.splitlines() if line.startswith("| Overrides |")
+ )
+ overrides = dict(
+ pair.split("=", 1) for pair in override_row.split("|")[2].strip().split(", ")
+ )
+ self.assertIs(ast.literal_eval(overrides["check_invariants"]), False)
+
def test_sample_action_idle_hazard_increases_entry_rate(self):
"""_sample_action() increases entry probability past idle cap.
RewardDiagnosticsWarning,
_binned_stats,
_compute_relationship_stats,
+ _duration_binned_stats,
bootstrap_confidence_intervals,
compute_distribution_shift_metrics,
distribution_diagnostics,
STATISTICAL,
TOLERANCE,
)
+from ..helpers import simulate_samples_with_defaults
from ..test_base import RewardSpaceTestBase
_perform_feature_analysis = getattr(reward_space_analysis, "_perform_feature_analysis", None)
class TestStatistics(RewardSpaceTestBase):
"""Statistical tests: metrics, diagnostics, bootstrap, correlations."""
+ def test_extended_duration_relationship_bins_preserve_mass_and_reward_means(self):
+ """Large configured durations retain all observations in truthful ratio bins."""
+ for duration in (10**306, 10**400):
+ with self.subTest(magnitude=duration.bit_length()):
+ df = simulate_samples_with_defaults(
+ self.base_params(max_trade_duration_candles=duration),
+ num_samples=32,
+ seed=42,
+ trading_mode="spot",
+ )
+ result = _compute_relationship_stats(df)
+ for name, target in (("idle_stats", "reward_idle"), ("hold_stats", "reward_hold")):
+ stats = result[name]
+ self.assertEqual(stats["count"].sum(), len(df))
+ self.assertEqual(stats.iloc[0]["count"], len(df))
+ self.assertAlmostEqual(stats.iloc[0]["mean"], df[target].mean(), places=6)
+ self.assertEqual(stats.index.name, "duration / max_trade_duration_candles")
+ self.assertEqual(stats.index[0], "[0, 0.25]")
+ self.assertEqual(stats.index[-1], "(2.75, 3]")
+
+ def test_extended_duration_bins_preserve_nullable_observation_semantics(self):
+ """Missing nullable duration observations stay excluded from reward aggregates."""
+ for dtype in ("Int64", "Float64"):
+ with self.subTest(dtype=dtype):
+ df = simulate_samples_with_defaults(
+ self.base_params(max_trade_duration_candles=10**400),
+ num_samples=32,
+ seed=42,
+ trading_mode="spot",
+ )
+ df["idle_duration"] = df["idle_duration"].astype(dtype)
+ df.loc[0, "idle_duration"] = pd.NA
+ stats = _compute_relationship_stats(df)["idle_stats"]
+ present = df["idle_duration"].notna()
+ self.assertEqual(stats["count"].sum(), int(present.sum()))
+ self.assertAlmostEqual(
+ stats.iloc[0]["mean"],
+ df.loc[present, "reward_idle"].mean(),
+ places=6,
+ )
+
+ def test_extended_duration_bins_distinguish_exact_right_closed_boundaries(self):
+ """Adjacent integer observations at huge boundaries must not round into one bin."""
+ duration = 10**400
+ values = [
+ 0,
+ 1,
+ duration // 4,
+ duration // 4 + 1,
+ duration // 2,
+ duration // 2 + 1,
+ 3 * duration,
+ 3 * duration + 1,
+ ]
+ df = pd.DataFrame({"duration": pd.Series(values, dtype=object), "reward": np.arange(8.0)})
+ result = _duration_binned_stats(df, "duration", "reward", duration)
+ self.assertEqual(result["count"].tolist(), [3, 2, 1] + [0] * 8 + [2])
+ self.assertEqual(result.iloc[0]["mean"], 1.0)
+ self.assertEqual(result.iloc[1]["mean"], 3.5)
+ self.assertEqual(result.iloc[2]["mean"], 5.0)
+ self.assertEqual(result.iloc[-1]["mean"], 6.5)
+
+ def test_ordinary_duration_relationship_bins_retain_candle_units(self):
+ """The normal configured scale keeps the existing candle-valued intervals."""
+ df = simulate_samples_with_defaults(
+ self.base_params(max_trade_duration_candles=128),
+ num_samples=32,
+ seed=42,
+ )
+ stats = _compute_relationship_stats(df)["idle_stats"]
+ self.assertEqual(stats.index.name, "bin")
+ first_upper = float(stats.index[0].split(",")[1].rstrip("]"))
+ last_upper = float(stats.index[-1].split(",")[1].rstrip("]"))
+ self.assertEqual(first_upper, 128 / 4)
+ self.assertEqual(last_upper, 3 * 128)
+ self.assertEqual(stats["count"].sum(), len(df))
+
def test_statistics_feature_analysis_skip_partial_dependence(self):
"""Invariant 107: skip_partial_dependence=True yields empty partial_deps."""
if _perform_feature_analysis is None:
import sys
import unittest
from pathlib import Path
+from unittest import mock
from coverage.config import DEFAULT_EXCLUDE, DEFAULT_PARTIAL, DEFAULT_PARTIAL_ALWAYS, CoverageConfig
from coverage.files import find_python_files
self.assertGreater(value, 0.0)
self.assertLessEqual(value, 100.0)
+ def test_the_floor_carries_its_measurement_provenance(self):
+ """A recorded measurement must be dated, final, and sufficient for its floor."""
+ lines = [line.strip() for line in self.text.splitlines()]
+ index = next(i for i, line in enumerate(lines) if line.partition("=")[0].strip() == FLOOR)
+ annotation = " ".join(lines[max(0, index - 2) : index])
+ self.assertIn("measured", annotation)
+ self.assertNotIn("PROVISIONAL", annotation)
+ self.assertRegex(annotation, r"\d{4}-\d{2}-\d{2}")
+ found = re.search(r"measured[^%]*?([0-9]+(?:\.[0-9]+)?)%", annotation)
+ self.assertIsNotNone(found, "the floor must record the measurement it relies on")
+ self.assertGreaterEqual(float(found.group(1)), float(self._raw_floor()))
+
def test_branch_tracing_is_enabled(self):
# Without it, every guard — an early return, a raise — counts as covered the
# moment its `if` is evaluated, and the gate measures nothing useful.
)
+class CoverageProvenanceRegressionTest(QaTestCase):
+ def test_actual_guard_rejects_invalid_and_accepts_final_dated_measurements(self):
+ """Run the real guard against fixture files, including stale but valid provenance."""
+ baseline = COVERAGERC.read_text()
+ cases = (
+ ("", False),
+ ("# measured 76.0%", False),
+ ("# measured 2026-10-02", False),
+ ("# measured 2026-10-02 at 69.9%", False),
+ ("# measured 2026-10-02 at 76.0% PROVISIONAL", False),
+ ("# measured 2026-10-02 at 70.0%", True),
+ ("# measured 2026-10-02 at 76.0%", True),
+ ("# measured 2000-01-01 at 76.0%", True),
+ )
+ for annotation, valid in cases:
+ with self.subTest(annotation=annotation), temporary_directory() as directory:
+ fixture = directory / ".coveragerc"
+ fixture.write_text(
+ "\n".join(
+ annotation if line.startswith("# measured") else line
+ for line in baseline.splitlines()
+ )
+ )
+ result = unittest.TestResult()
+ with mock.patch.dict(globals(), COVERAGERC=fixture):
+ CoverageFloorTest("test_the_floor_carries_its_measurement_provenance").run(
+ result
+ )
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(result.errors, [])
+ self.assertEqual(len(result.failures), 0 if valid else 1)
+
+
if __name__ == "__main__":
unittest.main()