From: Jérôme Benoit Date: Wed, 23 Sep 2026 19:03:59 +0000 (+0200) Subject: fix(reward): preserve profitable synthetic paths at high fees X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=30fadc845fdaa213816be0de2a972b291a2abd15;p=freqai-strategies.git fix(reward): preserve profitable synthetic paths at high fees --- diff --git a/ReforceXY/.basedpyright/diagnostics.json b/ReforceXY/.basedpyright/diagnostics.json index 52db651..0a03974 100644 --- a/ReforceXY/.basedpyright/diagnostics.json +++ b/ReforceXY/.basedpyright/diagnostics.json @@ -33,343 +33,343 @@ }, { "endCharacter": 5, - "endLine": 2102, + "endLine": 2108, "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": 2097 + "startLine": 2103 }, { "endCharacter": 21, - "endLine": 2099, + "endLine": 2105, "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": 2099 + "startLine": 2105 }, { "endCharacter": 5, - "endLine": 2124, + "endLine": 2130, "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": 2120 + "startLine": 2126 }, { "endCharacter": 43, - "endLine": 2121, + "endLine": 2127, "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": 2121 + "startLine": 2127 }, { "endCharacter": 42, - "endLine": 2121, + "endLine": 2127, "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": 2121 + "startLine": 2127 }, { "endCharacter": 60, - "endLine": 2381, + "endLine": 2387, "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": 2381 + "startLine": 2387 }, { "endCharacter": 58, - "endLine": 2382, + "endLine": 2388, "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": 2382 + "startLine": 2388 }, { "endCharacter": 88, - "endLine": 2401, + "endLine": 2407, "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": 2401 + "startLine": 2407 }, { "endCharacter": 88, - "endLine": 2401, + "endLine": 2407, "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": 2401 + "startLine": 2407 }, { "endCharacter": 17, - "endLine": 2410, + "endLine": 2416, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Object of type \"None\" cannot be called", "rule": "reportOptionalCall", "severity": "error", "startCharacter": 28, - "startLine": 2404 + "startLine": 2410 }, { "endCharacter": 40, - "endLine": 2639, + "endLine": 2645, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Argument of type \"Any | object | NAType | Unknown\" cannot be assigned to parameter \"arg1\" of type \"SupportsRichComparisonT@min\" in function \"min\"\n  Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n    Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n      Type \"object\" is not assignable to type \"SupportsRichComparison\"\n        \"object\" is incompatible with protocol \"SupportsDunderLT[Any]\"\n          \"__lt__\" is not present\n        \"object\" is incompatible with protocol \"SupportsDunderGT[Any]\"\n          \"__gt__\" is not present", "rule": "reportArgumentType", "severity": "error", "startCharacter": 22, - "startLine": 2639 + "startLine": 2645 }, { "endCharacter": 38, - "endLine": 2639, + "endLine": 2645, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Cannot access attribute \"min\" for class \"ExtensionArray\"\n  Attribute \"min\" is unknown", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 35, - "startLine": 2639 + "startLine": 2645 }, { "endCharacter": 59, - "endLine": 2639, + "endLine": 2645, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Argument of type \"Any | object | NAType | Unknown\" cannot be assigned to parameter \"arg2\" of type \"SupportsRichComparisonT@min\" in function \"min\"\n  Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n    Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n      Type \"object\" is not assignable to type \"SupportsRichComparison\"\n        \"object\" is incompatible with protocol \"SupportsDunderLT[Any]\"\n          \"__lt__\" is not present\n        \"object\" is incompatible with protocol \"SupportsDunderGT[Any]\"\n          \"__gt__\" is not present", "rule": "reportArgumentType", "severity": "error", "startCharacter": 42, - "startLine": 2639 + "startLine": 2645 }, { "endCharacter": 57, - "endLine": 2639, + "endLine": 2645, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Cannot access attribute \"min\" for class \"ExtensionArray\"\n  Attribute \"min\" is unknown", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 54, - "startLine": 2639 + "startLine": 2645 }, { "endCharacter": 40, - "endLine": 2640, + "endLine": 2646, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Argument of type \"Any | object | NAType | Unknown\" cannot be assigned to parameter \"arg1\" of type \"SupportsRichComparisonT@max\" in function \"max\"\n  Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n    Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n      Type \"object\" is not assignable to type \"SupportsRichComparison\"\n        \"object\" is incompatible with protocol \"SupportsDunderLT[Any]\"\n          \"__lt__\" is not present\n        \"object\" is incompatible with protocol \"SupportsDunderGT[Any]\"\n          \"__gt__\" is not present", "rule": "reportArgumentType", "severity": "error", "startCharacter": 22, - "startLine": 2640 + "startLine": 2646 }, { "endCharacter": 38, - "endLine": 2640, + "endLine": 2646, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Cannot access attribute \"max\" for class \"ExtensionArray\"\n  Attribute \"max\" is unknown", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 35, - "startLine": 2640 + "startLine": 2646 }, { "endCharacter": 59, - "endLine": 2640, + "endLine": 2646, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Argument of type \"Any | object | NAType | Unknown\" cannot be assigned to parameter \"arg2\" of type \"SupportsRichComparisonT@max\" in function \"max\"\n  Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n    Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n      Type \"object\" is not assignable to type \"SupportsRichComparison\"\n        \"object\" is incompatible with protocol \"SupportsDunderLT[Any]\"\n          \"__lt__\" is not present\n        \"object\" is incompatible with protocol \"SupportsDunderGT[Any]\"\n          \"__gt__\" is not present", "rule": "reportArgumentType", "severity": "error", "startCharacter": 42, - "startLine": 2640 + "startLine": 2646 }, { "endCharacter": 57, - "endLine": 2640, + "endLine": 2646, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Cannot access attribute \"max\" for class \"ExtensionArray\"\n  Attribute \"max\" is unknown", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 54, - "startLine": 2640 + "startLine": 2646 }, { "endCharacter": 76, - "endLine": 2656, + "endLine": 2662, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "No overloads for \"histogram\" match the provided arguments", "rule": "reportCallIssue", "severity": "error", "startCharacter": 24, - "startLine": 2656 + "startLine": 2662 }, { "endCharacter": 49, - "endLine": 2656, + "endLine": 2662, "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 \"_ArrayLikeComplex_co\" in function \"histogram\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"_ArrayLikeComplex_co\"\n    Type \"ExtensionArray\" is not assignable to type \"_ArrayLikeComplex_co\"\n      \"ExtensionArray\" is incompatible with protocol \"_SupportsArray[dtype[numpy.bool[builtins.bool] | number[Any, Any]]]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"_NestedSequence[_SupportsArray[dtype[numpy.bool[builtins.bool] | number[Any, Any]]]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 37, - "startLine": 2656 + "startLine": 2662 }, { "endCharacter": 74, - "endLine": 2657, + "endLine": 2663, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "No overloads for \"histogram\" match the provided arguments", "rule": "reportCallIssue", "severity": "error", "startCharacter": 23, - "startLine": 2657 + "startLine": 2663 }, { "endCharacter": 47, - "endLine": 2657, + "endLine": 2663, "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 \"_ArrayLikeComplex_co\" in function \"histogram\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"_ArrayLikeComplex_co\"\n    Type \"ExtensionArray\" is not assignable to type \"_ArrayLikeComplex_co\"\n      \"ExtensionArray\" is incompatible with protocol \"_SupportsArray[dtype[numpy.bool[builtins.bool] | number[Any, Any]]]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"_NestedSequence[_SupportsArray[dtype[numpy.bool[builtins.bool] | number[Any, Any]]]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 36, - "startLine": 2657 + "startLine": 2663 }, { "endCharacter": 51, - "endLine": 2685, + "endLine": 2691, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"u_values\" of type \"ToFloatND\" in function \"wasserstein_distance\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"ToFloatND\"\n    Type \"ExtensionArray\" is not assignable to type \"ToFloatND\"\n      \"ExtensionArray\" is incompatible with protocol \"_CanArrayND[floating_co]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"SequenceND[py_float | _CanArray[floating_co]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 39, - "startLine": 2685 + "startLine": 2691 }, { "endCharacter": 64, - "endLine": 2685, + "endLine": 2691, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"v_values\" of type \"ToFloatND\" in function \"wasserstein_distance\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"ToFloatND\"\n    Type \"ExtensionArray\" is not assignable to type \"ToFloatND\"\n      \"ExtensionArray\" is incompatible with protocol \"_CanArrayND[floating_co]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"SequenceND[py_float | _CanArray[floating_co]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 53, - "startLine": 2685 + "startLine": 2691 }, { "endCharacter": 68, - "endLine": 2688, + "endLine": 2694, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "No overloads for \"ks_2samp\" match the provided arguments", "rule": "reportCallIssue", "severity": "error", "startCharacter": 27, - "startLine": 2688 + "startLine": 2694 }, { "endCharacter": 54, - "endLine": 2688, + "endLine": 2694, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"data1\" of type \"ToFloatND\" in function \"ks_2samp\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"ToFloatND\"\n    Type \"ExtensionArray\" is not assignable to type \"ToFloatND\"\n      \"ExtensionArray\" is incompatible with protocol \"_CanArrayND[floating_co]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"SequenceND[py_float | _CanArray[floating_co]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 42, - "startLine": 2688 + "startLine": 2694 }, { "endCharacter": 67, - "endLine": 2688, + "endLine": 2694, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"data2\" of type \"ToFloatND\" in function \"ks_2samp\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"ToFloatND\"\n    Type \"ExtensionArray\" is not assignable to type \"ToFloatND\"\n      \"ExtensionArray\" is incompatible with protocol \"_CanArrayND[floating_co]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"SequenceND[py_float | _CanArray[floating_co]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 56, - "startLine": 2688 + "startLine": 2694 }, { "endCharacter": 29, - "endLine": 2988, + "endLine": 2994, "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": 2988 + "startLine": 2994 }, { "endCharacter": 28, - "endLine": 2988, + "endLine": 2994, "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": 2988 + "startLine": 2994 }, { "endCharacter": 65, - "endLine": 2997, + "endLine": 3003, "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": 2997 + "startLine": 3003 }, { "endCharacter": 65, - "endLine": 2997, + "endLine": 3003, "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": 2997 + "startLine": 3003 }, { "endCharacter": 5, - "endLine": 3893, + "endLine": 3899, "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": 3889 + "startLine": 3895 }, { "endCharacter": 43, - "endLine": 3890, + "endLine": 3896, "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": 3890 + "startLine": 3896 }, { "endCharacter": 42, - "endLine": 3890, + "endLine": 3896, "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": 3890 + "startLine": 3896 }, { "endCharacter": 22, diff --git a/ReforceXY/reward_space_analysis/README.md b/ReforceXY/reward_space_analysis/README.md index c5381a9..65744f3 100644 --- a/ReforceXY/reward_space_analysis/README.md +++ b/ReforceXY/reward_space_analysis/README.md @@ -165,10 +165,10 @@ Generates shift metrics for comparison (see Outputs section). - **`--real_episodes`** (path, optional) – Episodes pickle for real vs synthetic distribution shift metrics. (Simulation-only; triggers additional outputs when provided). -- **`--unrealized_pnl`** (flag, default: false) – Transform the retained - in-position synthetic price/PnL trajectory using fee-aware unrealized PnL. - This affects subsequent retained extrema and enabled reward terms that depend - on PnL. (Simulation-only.) +- **`--unrealized_pnl`** (flag, default: false) – Track a sampled market + price separately from the retained price within each trade; map its fee-aware + PnL through duration-based tanh scaling before retaining the mark. Retained + marks affect exit-efficiency extrema and PnL-dependent rewards. (Simulation-only.) ### Hybrid Simulation Scalars @@ -289,12 +289,15 @@ Let `max_u = max_unrealized_profit`, `min_u = min_unrealized_profit`, `efficiency_coefficient = 1 + efficiency_weight · (efficiency_center - ratio)` - Else: `efficiency_coefficient = 1` -The extrema start with the fee-adjusted PnL at the entry fill and then include -each retained market mark. In synthetic `unrealized_pnl` mode, a sampled -candidate discarded by the transform is not an extremum. -Synthetic marks are capped at +0.15; their lower bound is the lesser of -0.15 -and the fee-adjusted entry PnL. The extreme-PnL check also permits the -configured fee loss, rather than rejecting a valid short entry at high fees. +In synthetic `unrealized_pnl` mode, sampled market prices accumulate each +candle's return independently of the transformed, retained price. The +fee-aware sampled PnL is bounded, scaled by the duration-dependent tanh +factor, and converted to the retained price. Exit-efficiency extrema start +with the fee-adjusted PnL at the entry fill and then include each retained +mark; a sampled candidate is never retained as an extremum. Synthetic marks +are capped at +0.15; their lower bound is the lesser of -0.15 and the +fee-adjusted entry PnL. The extreme-PnL check permits that fee loss and +floating-point roundoff at its boundary, but rejects larger excursions. ##### Exit Attenuation diff --git a/ReforceXY/reward_space_analysis/reward_space_analysis.py b/ReforceXY/reward_space_analysis/reward_space_analysis.py index 412268b..eb8736b 100644 --- a/ReforceXY/reward_space_analysis/reward_space_analysis.py +++ b/ReforceXY/reward_space_analysis/reward_space_analysis.py @@ -1739,6 +1739,7 @@ def simulate_samples( # Synthetic market state current_open = 1.0 + sampled_open = current_open entry_open = current_open for _ in range(num_samples): @@ -1778,6 +1779,7 @@ def simulate_samples( position, entry_open=entry_open, current_open=entry_open, params=params ) max_unrealized_profit = min_unrealized_profit = entry_pnl + sampled_open = entry_open pnl_floor = min(-0.15, entry_pnl) else: idle_duration = 0 @@ -1810,33 +1812,36 @@ def simulate_samples( step_return = 0.0 step_return = float(np.clip(step_return, -0.95, 0.95)) - current_open = float(max(1e-6, current_open * (1.0 + step_return))) - # Always sample the random-walk price so both modes consume the same RNG stream. - # Unrealized-PnL mode replaces it below with the fee-aware price implied by - # the target PnL before reward calculation. + # Sample once in both modes; keep the raw market path separate from the + # transformed price while a position is open. + transform_unrealized_pnl = position in ( + Positions.Long, + Positions.Short, + ) and _get_bool_param(params, "unrealized_pnl", False) + if transform_unrealized_pnl: + sampled_open = float(max(1e-6, sampled_open * (1.0 + step_return))) + candidate_open = sampled_open + else: + current_open = float(max(1e-6, current_open * (1.0 + step_return))) + candidate_open = current_open if position in (Positions.Long, Positions.Short): candidate_pnl = float( np.clip( _compute_unrealized_pnl_estimate( position, entry_open=entry_open, - current_open=current_open, + current_open=candidate_open, params=params, ), pnl_floor, 0.15, ) ) - if _get_bool_param(params, "unrealized_pnl", False): - # Let the sampled market move shape the next retained PnL without - # storing the discarded candidate in exit-efficiency extrema. - prospective_max = max(max_unrealized_profit, candidate_pnl) - prospective_min = min(min_unrealized_profit, candidate_pnl) - center_unrealized = 0.5 * (prospective_max + prospective_min) + if transform_unrealized_pnl: beta = _get_float_param(params, "pnl_amplification_sensitivity") hold_ratio = _compute_duration_ratio(trade_duration, max_trade_duration_candles) target_pnl = float( - np.clip(center_unrealized * math.tanh(beta * hold_ratio), pnl_floor, 0.15) + np.clip(candidate_pnl * math.tanh(beta * hold_ratio), pnl_floor, 0.15) ) entry_fee_rate, exit_fee_rate = _get_fee_rates(params) if position == Positions.Long: @@ -2031,7 +2036,8 @@ def _validate_simulation_invariants(df: pd.DataFrame, params: RewardParams) -> N ) # INVARIANT 5: Bounded values - extreme_pnl = df[(df["pnl"].abs() > thr_extreme)] + # Fee-adjusted prices can differ from the nominal fee product by a few ULPs. + extreme_pnl = df[df["pnl"].abs() > thr_extreme + eps_pnl] if len(extreme_pnl) > 0: max_abs_pnl = float(df["pnl"].abs().max()) raise AssertionError( diff --git a/ReforceXY/reward_space_analysis/tests/README.md b/ReforceXY/reward_space_analysis/tests/README.md index 457742f..e4203b2 100644 --- a/ReforceXY/reward_space_analysis/tests/README.md +++ b/ReforceXY/reward_space_analysis/tests/README.md @@ -216,6 +216,9 @@ Columns: | 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 | ### Non-Owning Smoke / Reference Checks diff --git a/ReforceXY/reward_space_analysis/tests/pbrs/test_pbrs.py b/ReforceXY/reward_space_analysis/tests/pbrs/test_pbrs.py index 2b600bf..7f2eb92 100644 --- a/ReforceXY/reward_space_analysis/tests/pbrs/test_pbrs.py +++ b/ReforceXY/reward_space_analysis/tests/pbrs/test_pbrs.py @@ -296,6 +296,148 @@ class TestSimulationParity(RewardSpaceTestBase): places=8, ) + def test_synthetic_high_fee_winner_retains_profitable_mark(self): + """Keep favorable retained PnL and exit reward positive with high fees. + + **Invariant:** pbrs-synthetic-profitable-mark-133 + """ + for direction, innovation in (("long", 0.299), ("short", -0.299)): + for flat_first in (False, True): + with self.subTest(direction=direction, flat_first=flat_first): + params = self.base_params( + unrealized_pnl=True, + entry_fee_rate=0.1, + exit_fee_rate=0.1, + max_trade_duration_candles=1, + ) + enter = Actions.Long_enter if direction == "long" else Actions.Short_enter + exit_action = Actions.Long_exit if direction == "long" else Actions.Short_exit + actions = [(enter, 1.0, 0.0, 0.0)] + # The directional drift completes a 30% favorable market move. + innovations = [innovation] + if flat_first: + actions.append((Actions.Neutral, 0.0, 0.0, 1.0)) + innovations = [-0.001 if direction == "long" else 0.001, innovation] + actions.append((exit_action, 0.0, 1.0, 0.0)) + innovations.append(0.0) + with ( + patch.object(reward_space_analysis, "_sample_action", side_effect=actions), + patch.object( + reward_space_analysis.random.Random, + "gauss", + side_effect=innovations, + ), + ): + samples = simulate_samples( + num_samples=len(actions), + seed=SEEDS.BASE, + params=params, + base_factor=PARAMS.BASE_FACTOR, + profit_aim=PARAMS.PROFIT_AIM, + risk_reward_ratio=PARAMS.RISK_REWARD_RATIO, + max_duration_ratio=2.0, + trading_mode="futures", + pnl_base_std=PARAMS.PNL_STD, + pnl_duration_vol_scale=PARAMS.PNL_DUR_VOL_SCALE, + ) + if flat_first: + self.assertLess(float(samples.iloc[0]["next_pnl"]), 0.0) + self.assertGreater(float(samples.iloc[-2]["next_pnl"]), 0.0) + self.assertGreater(float(samples.iloc[-1]["exit_pnl"]), 0.0) + self.assertGreater(float(samples.iloc[-1]["reward_exit"]), 0.0) + + def test_synthetic_high_fee_short_boundary_rejects_real_excess(self): + """Accept rounding at the fee bound but reject a material loss beyond it. + + **Invariant:** pbrs-synthetic-fee-boundary-134 + """ + params = self.base_params( + entry_fee_rate=0.1, + exit_fee_rate=0.1, + max_trade_duration_candles=1, + ) + with ( + patch.object( + reward_space_analysis, + "_sample_action", + side_effect=[ + (Actions.Neutral, 0.0, 0.0, 1.0), + (Actions.Short_enter, 1.0, 0.0, 0.0), + (Actions.Short_exit, 0.0, 1.0, 0.0), + ], + ), + patch.object( + reward_space_analysis.random.Random, + "gauss", + # Neutral move sets entry price to 1.03; next move cancels short drift. + side_effect=[0.03, 0.001, 0.0], + ), + ): + samples = simulate_samples( + num_samples=3, + seed=SEEDS.BASE, + params=params, + base_factor=PARAMS.BASE_FACTOR, + profit_aim=PARAMS.PROFIT_AIM, + risk_reward_ratio=PARAMS.RISK_REWARD_RATIO, + max_duration_ratio=2.0, + trading_mode="futures", + pnl_base_std=PARAMS.PNL_STD, + pnl_duration_vol_scale=PARAMS.PNL_DUR_VOL_SCALE, + ) + fee_factor = (1.0 + params["entry_fee_rate"]) * (1.0 + params["exit_fee_rate"]) + self.assertAlmostEqual(float(samples.iloc[-1]["pnl"]), 1.0 - fee_factor, places=12) + beyond_bound = samples.copy() + beyond_bound.loc[beyond_bound.index[-1], "pnl"] = 1.0 - fee_factor - 0.001 + with self.assertRaisesRegex(AssertionError, "extreme PnL"): + reward_space_analysis._validate_simulation_invariants(beyond_bound, params) + + def test_unrealized_pnl_retains_sampled_market_path_after_candidate_cap(self): + """Keep distinct market paths after their first retained PnL is capped. + + **Invariant:** pbrs-synthetic-latent-price-135 + """ + params = self.base_params(unrealized_pnl=True, max_trade_duration_candles=1) + actions = [ + (Actions.Long_enter, 1.0, 0.0, 0.0), + (Actions.Neutral, 0.0, 0.0, 1.0), + (Actions.Long_exit, 0.0, 1.0, 0.0), + ] + + def sample(first_return: float) -> pd.DataFrame: + with ( + patch.object(reward_space_analysis, "_sample_action", side_effect=actions), + patch.object( + reward_space_analysis.random.Random, + "gauss", + # Both first candidates clip at +0.15; the following move is -30%. + side_effect=[first_return, -0.301, 0.0], + ), + ): + return simulate_samples( + num_samples=len(actions), + seed=SEEDS.BASE, + params=params, + base_factor=PARAMS.BASE_FACTOR, + profit_aim=PARAMS.PROFIT_AIM, + risk_reward_ratio=PARAMS.RISK_REWARD_RATIO, + max_duration_ratio=2.0, + trading_mode="futures", + pnl_base_std=PARAMS.PNL_STD, + pnl_duration_vol_scale=PARAMS.PNL_DUR_VOL_SCALE, + ) + + lower_price = sample(0.599) + higher_price = sample(0.949) + self.assertAlmostEqual( + float(lower_price.iloc[0]["next_pnl"]), float(higher_price.iloc[0]["next_pnl"]) + ) + self.assertGreater(float(lower_price.iloc[1]["next_pnl"]), 0.0) + self.assertGreater( + float(higher_price.iloc[1]["next_pnl"]), + float(lower_price.iloc[1]["next_pnl"]), + ) + def test_unrealized_pnl_uses_each_sampled_market_move(self): """Later Gaussian innovations affect later retained PnL without becoming extrema.""" params = self.base_params(