From: Jérôme Benoit Date: Tue, 20 Oct 2026 21:06:24 +0000 (+0200) Subject: fix(reforcexy): align reward costs and model selection (#294) X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=017f889c1b70580e4baa0c8af2682abcbd9f6fa2;p=freqai-strategies.git fix(reforcexy): align reward costs and model selection (#294) Retain temporal costs on invalid actions and select policies on non-PBRS evaluation scores. Advance reward compatibility, correct terminal analytical transitions and synthetic duration bounds, and reuse canonical architecture and analytical definitions. Update the mathematical reference, behavioral regressions and BasedPyright snapshot. --- diff --git a/ReforceXY/.basedpyright/diagnostics.json b/ReforceXY/.basedpyright/diagnostics.json index 146010e..fa570c2 100644 --- a/ReforceXY/.basedpyright/diagnostics.json +++ b/ReforceXY/.basedpyright/diagnostics.json @@ -33,1053 +33,1053 @@ }, { "endCharacter": 5, - "endLine": 2210, + "endLine": 2235, "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": 2205 + "startLine": 2230 }, { "endCharacter": 21, - "endLine": 2207, + "endLine": 2232, "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": 2207 + "startLine": 2232 }, { "endCharacter": 5, - "endLine": 2273, + "endLine": 2298, "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": 2269 + "startLine": 2294 }, { "endCharacter": 43, - "endLine": 2270, + "endLine": 2295, "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": 2270 + "startLine": 2295 }, { "endCharacter": 42, - "endLine": 2270, + "endLine": 2295, "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": 2270 + "startLine": 2295 }, { "endCharacter": 60, - "endLine": 2532, + "endLine": 2557, "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": 2532 + "startLine": 2557 }, { "endCharacter": 58, - "endLine": 2533, + "endLine": 2558, "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": 2533 + "startLine": 2558 }, { "endCharacter": 88, - "endLine": 2552, + "endLine": 2577, "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": 2552 + "startLine": 2577 }, { "endCharacter": 88, - "endLine": 2552, + "endLine": 2577, "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": 2552 + "startLine": 2577 }, { "endCharacter": 17, - "endLine": 2561, + "endLine": 2586, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Object of type \"None\" cannot be called", "rule": "reportOptionalCall", "severity": "error", "startCharacter": 28, - "startLine": 2555 + "startLine": 2580 }, { "endCharacter": 29, - "endLine": 3162, + "endLine": 3187, "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": 3162 + "startLine": 3187 }, { "endCharacter": 28, - "endLine": 3162, + "endLine": 3187, "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": 3162 + "startLine": 3187 }, { "endCharacter": 65, - "endLine": 3171, + "endLine": 3196, "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": 3171 + "startLine": 3196 }, { "endCharacter": 65, - "endLine": 3171, + "endLine": 3196, "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": 3171 + "startLine": 3196 }, { "endCharacter": 5, - "endLine": 4083, + "endLine": 4108, "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": 4079 + "startLine": 4104 }, { "endCharacter": 43, - "endLine": 4080, + "endLine": 4105, "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": 4080 + "startLine": 4105 }, { "endCharacter": 42, - "endLine": 4080, + "endLine": 4105, "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": 4080 + "startLine": 4105 }, { "endCharacter": 22, - "endLine": 1086, + "endLine": 1122, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Cannot assign to attribute \"train_env\" for class \"ReforceXY*\"\n  Expression of type \"VecEnv\" cannot be assigned to attribute \"train_env\" of class \"ReforceXY\"\n    Type \"VecEnv\" is not assignable to type \"VecMonitor | SubprocVecEnv | Env[Unknown, Unknown]\"\n      \"VecEnv\" is not assignable to \"VecMonitor\"\n      \"VecEnv\" is not assignable to \"SubprocVecEnv\"\n      \"VecEnv\" is not assignable to \"Env[Unknown, Unknown]\"", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 13, - "startLine": 1086 + "startLine": 1122 }, { "endCharacter": 37, - "endLine": 1086, + "endLine": 1122, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Cannot assign to attribute \"eval_env\" for class \"ReforceXY*\"\n  Expression of type \"VecEnv | None\" cannot be assigned to attribute \"eval_env\" of class \"ReforceXY\"\n    Type \"VecEnv | None\" is not assignable to type \"VecMonitor | SubprocVecEnv | Env[Unknown, Unknown]\"\n      Type \"VecEnv\" is not assignable to type \"VecMonitor | SubprocVecEnv | Env[Unknown, Unknown]\"\n        \"VecEnv\" is not assignable to \"VecMonitor\"\n        \"VecEnv\" is not assignable to \"SubprocVecEnv\"\n        \"VecEnv\" is not assignable to \"Env[Unknown, Unknown]\"", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 29, - "startLine": 1086 + "startLine": 1122 }, { "endCharacter": 24, - "endLine": 1347, + "endLine": 1384, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"VecEnv\" cannot be assigned to parameter \"eval_env\" of type \"BaseEnvironment\" in function \"__init__\"\n  \"VecEnv\" is not assignable to \"BaseEnvironment\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 16, - "startLine": 1347 + "startLine": 1384 }, { "endCharacter": 38, - "endLine": 1580, + "endLine": 1617, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"obj\" of type \"Sized\" in function \"len\"\n  Type \"Any | None\" is not assignable to type \"Sized\"\n    \"None\" is incompatible with protocol \"Sized\"\n      \"__len__\" is not present", "rule": "reportArgumentType", "severity": "error", "startCharacter": 30, - "startLine": 1580 + "startLine": 1617 }, { "endCharacter": 36, - "endLine": 1584, + "endLine": 1621, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"obj\" of type \"Sized\" in function \"len\"\n  Type \"Any | None\" is not assignable to type \"Sized\"\n    \"None\" is incompatible with protocol \"Sized\"\n      \"__len__\" is not present", "rule": "reportArgumentType", "severity": "error", "startCharacter": 29, - "startLine": 1584 + "startLine": 1621 }, { "endCharacter": 80, - "endLine": 1587, + "endLine": 1624, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"timeframe\" of type \"str\" in function \"steps_to_days\"\n  Type \"Any | None\" is not assignable to type \"str\"\n    \"None\" is not assignable to \"str\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 52, - "startLine": 1587 + "startLine": 1624 }, { "endCharacter": 78, - "endLine": 1588, + "endLine": 1625, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"timeframe\" of type \"str\" in function \"steps_to_days\"\n  Type \"Any | None\" is not assignable to type \"str\"\n    \"None\" is not assignable to \"str\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 50, - "startLine": 1588 + "startLine": 1625 }, { "endCharacter": 80, - "endLine": 1589, + "endLine": 1626, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"timeframe\" of type \"str\" in function \"steps_to_days\"\n  Type \"Any | None\" is not assignable to type \"str\"\n    \"None\" is not assignable to \"str\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 52, - "startLine": 1589 + "startLine": 1626 }, { "endCharacter": 56, - "endLine": 1673, + "endLine": 1710, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"VecMonitor | SubprocVecEnv | Env[Unknown, Unknown]\" cannot be assigned to parameter \"eval_env\" of type \"VecEnv | None\" in function \"get_callbacks\"\n  Type \"VecMonitor | SubprocVecEnv | Env[Unknown, Unknown]\" is not assignable to type \"VecEnv | None\"\n    Type \"Env[Unknown, Unknown]\" is not assignable to type \"VecEnv | None\"\n      \"Env[Unknown, Unknown]\" is not assignable to \"VecEnv\"\n      \"Env[Unknown, Unknown]\" is not assignable to \"None\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 43, - "startLine": 1673 + "startLine": 1710 }, { "endCharacter": 84, - "endLine": 1693, + "endLine": 1730, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | list[float]\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | list[float]\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"list[float]\" is not assignable to type \"ConvertibleToFloat\"\n      \"list[float]\" is not assignable to \"str\"\n      \"list[float]\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"list[float]\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"list[float]\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 67, - "startLine": 1693 + "startLine": 1730 }, { "endCharacter": 84, - "endLine": 1693, + "endLine": 1730, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | list[float]\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | list[float]\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"list[float]\" is not assignable to type \"ConvertibleToFloat\"\n      \"list[float]\" is not assignable to \"str\"\n      \"list[float]\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"list[float]\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"list[float]\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 67, - "startLine": 1693 + "startLine": 1730 }, { "endCharacter": 44, - "endLine": 2771, + "endLine": 2808, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "\"best_trial_params\" is possibly unbound", "rule": "reportPossiblyUnboundVariable", "severity": "error", "startCharacter": 27, - "startLine": 2771 + "startLine": 2808 }, { "endCharacter": 12, - "endLine": 2791, + "endLine": 2828, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Parameter declaration \"seed\" is obscured by a declaration of the same name", "rule": "reportRedeclaration", "severity": "error", "startCharacter": 8, - "startLine": 2791 + "startLine": 2828 }, { "endCharacter": 16, - "endLine": 2792, + "endLine": 2829, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Parameter declaration \"env_info\" is obscured by a declaration of the same name", "rule": "reportRedeclaration", "severity": "error", "startCharacter": 8, - "startLine": 2792 + "startLine": 2829 }, { "endCharacter": 51, - "endLine": 2837, + "endLine": 2874, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"list[() -> BaseEnvironment]\" cannot be assigned to parameter \"env_fns\" of type \"list[() -> Env[Unknown, Unknown]]\" in function \"__init__\"\n  \"list[() -> BaseEnvironment]\" is not assignable to \"list[() -> Env[Unknown, Unknown]]\"\n    Type parameter \"_T@list\" is invariant, but \"() -> BaseEnvironment\" is not the same as \"() -> Env[Unknown, Unknown]\"\n    Consider switching from \"list\" to \"Sequence\" which is covariant", "rule": "reportArgumentType", "severity": "error", "startCharacter": 42, - "startLine": 2837 + "startLine": 2874 }, { "endCharacter": 53, - "endLine": 2843, + "endLine": 2880, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"list[() -> BaseEnvironment]\" cannot be assigned to parameter \"env_fns\" of type \"list[() -> Env[Unknown, Unknown]]\" in function \"__init__\"\n  \"list[() -> BaseEnvironment]\" is not assignable to \"list[() -> Env[Unknown, Unknown]]\"\n    Type parameter \"_T@list\" is invariant, but \"() -> BaseEnvironment\" is not the same as \"() -> Env[Unknown, Unknown]\"\n    Consider switching from \"list\" to \"Sequence\" which is covariant", "rule": "reportArgumentType", "severity": "error", "startCharacter": 45, - "startLine": 2843 + "startLine": 2880 }, { "endCharacter": 22, - "endLine": 2906, + "endLine": 2943, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Operator \"*\" not supported for \"None\"", "rule": "reportOptionalOperand", "severity": "error", "startCharacter": 15, - "startLine": 2906 + "startLine": 2943 }, { "endCharacter": 96, - "endLine": 2908, + "endLine": 2945, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Operator \"*\" not supported for \"None\"", "rule": "reportOptionalOperand", "severity": "error", "startCharacter": 89, - "startLine": 2908 + "startLine": 2945 }, { "endCharacter": 23, - "endLine": 2911, + "endLine": 2948, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Operator \"*\" not supported for \"None\"", "rule": "reportOptionalOperand", "severity": "error", "startCharacter": 16, - "startLine": 2911 + "startLine": 2948 }, { "endCharacter": 98, - "endLine": 2913, + "endLine": 2950, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Operator \"*\" not supported for \"None\"", "rule": "reportOptionalOperand", "severity": "error", "startCharacter": 91, - "startLine": 2913 + "startLine": 2950 }, { "endCharacter": 44, - "endLine": 2925, + "endLine": 2962, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Operator \"*\" not supported for types \"Any | None\" and \"Any | int | None\"\n  Operator \"*\" not supported for types \"None\" and \"None\"\n  Operator \"*\" not supported for types \"None\" and \"int\"", "rule": "reportOperatorIssue", "severity": "error", "startCharacter": 15, - "startLine": 2925 + "startLine": 2962 }, { "endCharacter": 133, - "endLine": 2927, + "endLine": 2964, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Operator \"*\" not supported for types \"Any | None\" and \"Any | int | None\"\n  Operator \"*\" not supported for types \"None\" and \"None\"\n  Operator \"*\" not supported for types \"None\" and \"int\"", "rule": "reportOperatorIssue", "severity": "error", "startCharacter": 106, - "startLine": 2927 + "startLine": 2964 }, { "endCharacter": 30, - "endLine": 2930, + "endLine": 2967, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Operator \">\" not supported for \"None\"", "rule": "reportOptionalOperand", "severity": "error", "startCharacter": 15, - "startLine": 2930 + "startLine": 2967 }, { "endCharacter": 54, - "endLine": 2996, + "endLine": 3033, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "\"is_pruned\" is not a known attribute of \"None\"", "rule": "reportOptionalMemberAccess", "severity": "error", "startCharacter": 45, - "startLine": 2996 + "startLine": 3033 }, { "endCharacter": 67, - "endLine": 2999, + "endLine": 3036, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "\"use_masking\" is not a known attribute of \"None\"", "rule": "reportOptionalMemberAccess", "severity": "error", "startCharacter": 56, - "startLine": 2999 + "startLine": 3036 }, { "endCharacter": 60, - "endLine": 3007, + "endLine": 3044, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "\"update_best_reward\" is not a known attribute of \"None\"", "rule": "reportOptionalMemberAccess", "severity": "error", "startCharacter": 42, - "startLine": 3007 + "startLine": 3044 }, { "endCharacter": 84, - "endLine": 3007, + "endLine": 3044, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | list[float]\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | list[float]\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"list[float]\" is not assignable to type \"ConvertibleToFloat\"\n      \"list[float]\" is not assignable to \"str\"\n      \"list[float]\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"list[float]\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"list[float]\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 67, - "startLine": 3007 + "startLine": 3044 }, { "endCharacter": 84, - "endLine": 3007, + "endLine": 3044, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | list[float]\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | list[float]\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"list[float]\" is not assignable to type \"ConvertibleToFloat\"\n      \"list[float]\" is not assignable to \"str\"\n      \"list[float]\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"list[float]\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"list[float]\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 67, - "startLine": 3007 + "startLine": 3044 }, { "endCharacter": 46, - "endLine": 3061, + "endLine": 3098, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "\"is_pruned\" is not a known attribute of \"None\"", "rule": "reportOptionalMemberAccess", "severity": "error", "startCharacter": 37, - "startLine": 3061 + "startLine": 3098 }, { "endCharacter": 57, - "endLine": 3064, + "endLine": 3101, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "\"best_mean_reward\" is not a known attribute of \"None\"", "rule": "reportOptionalMemberAccess", "severity": "error", "startCharacter": 41, - "startLine": 3064 + "startLine": 3101 }, { "endCharacter": 37, - "endLine": 3074, + "endLine": 3111, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Cannot assign to attribute \"train_env\" for class \"ReforceXY*\"\n  Type \"None\" is not assignable to type \"VecMonitor | SubprocVecEnv | Env[Unknown, Unknown]\"\n    \"None\" is not assignable to \"VecMonitor\"\n    \"None\" is not assignable to \"SubprocVecEnv\"\n    \"None\" is not assignable to \"Env[Unknown, Unknown]\"", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 33, - "startLine": 3074 + "startLine": 3111 }, { "endCharacter": 36, - "endLine": 3079, + "endLine": 3116, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Cannot assign to attribute \"eval_env\" for class \"ReforceXY*\"\n  Type \"None\" is not assignable to type \"VecMonitor | SubprocVecEnv | Env[Unknown, Unknown]\"\n    \"None\" is not assignable to \"VecMonitor\"\n    \"None\" is not assignable to \"SubprocVecEnv\"\n    \"None\" is not assignable to \"Env[Unknown, Unknown]\"", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 32, - "startLine": 3079 + "startLine": 3116 }, { "endCharacter": 7, - "endLine": 3124, + "endLine": 3161, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Declaration \"MyRLEnv\" is obscured by a declaration of the same name", "rule": "reportRedeclaration", "severity": "error", "startCharacter": 0, - "startLine": 3124 + "startLine": 3161 }, { "endCharacter": 36, - "endLine": 3136, + "endLine": 3173, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Declaration \"_last_closed_trade_tick\" is obscured by a declaration of the same name", "rule": "reportRedeclaration", "severity": "error", "startCharacter": 13, - "startLine": 3136 + "startLine": 3173 }, { "endCharacter": 13, - "endLine": 3728, + "endLine": 3765, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Method \"reset\" overrides class \"BaseEnvironment\" in an incompatible manner\n  Return type mismatch: base method returns type \"tuple[DataFrame, dict[Unknown, Unknown]]\", override returns type \"tuple[NDArray[float32], dict[str, Any]]\"\n    \"tuple[NDArray[float32], dict[str, Any]]\" is not assignable to \"tuple[DataFrame, dict[Unknown, Unknown]]\"\n      Tuple entry 1 is incorrect type\n        \"ndarray[_AnyShape, dtype[float32]]\" is not assignable to \"DataFrame\"", "rule": "reportIncompatibleMethodOverride", "severity": "error", "startCharacter": 8, - "startLine": 3728 + "startLine": 3765 }, { "endCharacter": 26, - "endLine": 3749, + "endLine": 3786, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Type \"tuple[DataFrame, dict[Unknown, Unknown]]\" is not assignable to return type \"tuple[NDArray[float32], dict[str, Any]]\"\n  \"DataFrame\" is not assignable to \"ndarray[_AnyShape, dtype[float32]]\"", "rule": "reportReturnType", "severity": "error", "startCharacter": 15, - "startLine": 3749 + "startLine": 3786 }, { "endCharacter": 55, - "endLine": 4057, + "endLine": 4091, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 44, - "startLine": 4057 + "startLine": 4091 }, { "endCharacter": 55, - "endLine": 4057, + "endLine": 4091, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 44, - "startLine": 4057 + "startLine": 4091 }, { "endCharacter": 59, - "endLine": 4081, + "endLine": 4115, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 48, - "startLine": 4081 + "startLine": 4115 }, { "endCharacter": 59, - "endLine": 4081, + "endLine": 4115, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 48, - "startLine": 4081 + "startLine": 4115 }, { "endCharacter": 24, - "endLine": 4110, + "endLine": 4144, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Method \"_get_observation\" overrides class \"BaseEnvironment\" in an incompatible manner\n  Return type mismatch: base method returns type \"DataFrame\", override returns type \"NDArray[float32]\"\n    \"ndarray[_AnyShape, dtype[float32]]\" is not assignable to \"DataFrame\"", "rule": "reportIncompatibleMethodOverride", "severity": "error", "startCharacter": 8, - "startLine": 4110 + "startLine": 4144 }, { "endCharacter": 12, - "endLine": 4203, + "endLine": 4237, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Method \"step\" overrides class \"Base5ActionRLEnv\" in an incompatible manner\n  Return type mismatch: base method returns type \"tuple[DataFrame, float, bool, Literal[False], dict[str, Unknown]]\", override returns type \"tuple[NDArray[float32], float, bool, bool, dict[str, Any]]\"\n    \"tuple[NDArray[float32], float, bool, bool, dict[str, Any]]\" is not assignable to \"tuple[DataFrame, float, bool, Literal[False], dict[str, Unknown]]\"\n      Tuple entry 1 is incorrect type\n        \"ndarray[_AnyShape, dtype[float32]]\" is not assignable to \"DataFrame\"", "rule": "reportIncompatibleMethodOverride", "severity": "error", "startCharacter": 8, - "startLine": 4203 + "startLine": 4237 }, { "endCharacter": 20, - "endLine": 4393, + "endLine": 4432, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Method \"action_masks\" overrides class \"BaseEnvironment\" in an incompatible manner\n  Return type mismatch: base method returns type \"list[bool]\", override returns type \"NDArray[bool_]\"\n    \"ndarray[_AnyShape, dtype[bool_]]\" is not assignable to \"list[bool]\"", "rule": "reportIncompatibleMethodOverride", "severity": "error", "startCharacter": 8, - "startLine": 4393 + "startLine": 4432 }, { "endCharacter": 40, - "endLine": 4542, + "endLine": 4581, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "\"Figure\" is not exported from module \"matplotlib.pyplot\"", "rule": "reportPrivateImportUsage", "severity": "error", "startCharacter": 34, - "startLine": 4542 + "startLine": 4581 }, { "endCharacter": 54, - "endLine": 5027, + "endLine": 5066, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 45, - "startLine": 5027 + "startLine": 5066 }, { "endCharacter": 54, - "endLine": 5027, + "endLine": 5066, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 45, - "startLine": 5027 + "startLine": 5066 }, { "endCharacter": 50, - "endLine": 5039, + "endLine": 5078, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"object\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"object\" is not assignable to type \"ConvertibleToFloat\"\n    \"object\" is not assignable to \"str\"\n    \"object\" is incompatible with protocol \"Buffer\"\n      \"__buffer__\" is not present\n    \"object\" is incompatible with protocol \"SupportsFloat\"\n      \"__float__\" is not present\n    \"object\" is incompatible with protocol \"SupportsIndex\"\n      \"__index__\" is not present", "rule": "reportArgumentType", "severity": "error", "startCharacter": 37, - "startLine": 5039 + "startLine": 5078 }, { "endCharacter": 72, - "endLine": 5050, + "endLine": 5089, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 70, - "startLine": 5050 + "startLine": 5089 }, { "endCharacter": 72, - "endLine": 5050, + "endLine": 5089, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 70, - "startLine": 5050 + "startLine": 5089 }, { "endCharacter": 73, - "endLine": 5059, + "endLine": 5098, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 71, - "startLine": 5059 + "startLine": 5098 }, { "endCharacter": 73, - "endLine": 5059, + "endLine": 5098, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 71, - "startLine": 5059 + "startLine": 5098 }, { "endCharacter": 58, - "endLine": 5068, + "endLine": 5107, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 56, - "startLine": 5068 + "startLine": 5107 }, { "endCharacter": 58, - "endLine": 5068, + "endLine": 5107, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 56, - "startLine": 5068 + "startLine": 5107 }, { "endCharacter": 50, - "endLine": 5299, + "endLine": 5338, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Mapping[Unknown, Unknown]\" cannot be assigned to parameter \"src\" of type \"dict[str, Any]\" in function \"deepmerge\"\n  \"Mapping[Unknown, Unknown]\" is not assignable to \"dict[str, Any]\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 49, - "startLine": 5299 + "startLine": 5338 }, { "endCharacter": 20, - "endLine": 5479, + "endLine": 5497, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 18, - "startLine": 5479 + "startLine": 5497 }, { "endCharacter": 20, - "endLine": 5479, + "endLine": 5497, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 18, - "startLine": 5479 + "startLine": 5497 }, { "endCharacter": 59, - "endLine": 5484, + "endLine": 5502, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 31, - "startLine": 5484 + "startLine": 5502 }, { "endCharacter": 59, - "endLine": 5484, + "endLine": 5502, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 31, - "startLine": 5484 + "startLine": 5502 }, { "endCharacter": 65, - "endLine": 5485, + "endLine": 5503, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 34, - "startLine": 5485 + "startLine": 5503 }, { "endCharacter": 65, - "endLine": 5485, + "endLine": 5503, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 34, - "startLine": 5485 + "startLine": 5503 }, { "endCharacter": 63, - "endLine": 5488, + "endLine": 5506, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 34, - "startLine": 5488 + "startLine": 5506 }, { "endCharacter": 63, - "endLine": 5488, + "endLine": 5506, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 34, - "startLine": 5488 + "startLine": 5506 }, { "endCharacter": 61, - "endLine": 5490, + "endLine": 5508, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 32, - "startLine": 5490 + "startLine": 5508 }, { "endCharacter": 61, - "endLine": 5490, + "endLine": 5508, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 32, - "startLine": 5490 + "startLine": 5508 }, { "endCharacter": 67, - "endLine": 5491, + "endLine": 5509, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 36, - "startLine": 5491 + "startLine": 5509 }, { "endCharacter": 67, - "endLine": 5491, + "endLine": 5509, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 36, - "startLine": 5491 + "startLine": 5509 }, { "endCharacter": 73, - "endLine": 5492, + "endLine": 5510, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 39, - "startLine": 5492 + "startLine": 5510 }, { "endCharacter": 73, - "endLine": 5492, + "endLine": 5510, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 39, - "startLine": 5492 + "startLine": 5510 }, { "endCharacter": 61, - "endLine": 5493, + "endLine": 5511, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 33, - "startLine": 5493 + "startLine": 5511 }, { "endCharacter": 61, - "endLine": 5493, + "endLine": 5511, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 33, - "startLine": 5493 + "startLine": 5511 }, { "endCharacter": 89, - "endLine": 5500, + "endLine": 5518, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 52, - "startLine": 5500 + "startLine": 5518 }, { "endCharacter": 89, - "endLine": 5500, + "endLine": 5518, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 52, - "startLine": 5500 + "startLine": 5518 }, { "endCharacter": 83, - "endLine": 5501, + "endLine": 5519, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 49, - "startLine": 5501 + "startLine": 5519 }, { "endCharacter": 83, - "endLine": 5501, + "endLine": 5519, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 49, - "startLine": 5501 + "startLine": 5519 }, { "endCharacter": 65, - "endLine": 5527, + "endLine": 5545, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 34, - "startLine": 5527 + "startLine": 5545 }, { "endCharacter": 65, - "endLine": 5527, + "endLine": 5545, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 34, - "startLine": 5527 + "startLine": 5545 }, { "endCharacter": 67, - "endLine": 5529, + "endLine": 5547, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 35, - "startLine": 5529 + "startLine": 5547 }, { "endCharacter": 67, - "endLine": 5529, + "endLine": 5547, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 35, - "startLine": 5529 + "startLine": 5547 }, { "endCharacter": 87, - "endLine": 5532, + "endLine": 5550, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 46, - "startLine": 5532 + "startLine": 5550 }, { "endCharacter": 87, - "endLine": 5532, + "endLine": 5550, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 46, - "startLine": 5532 + "startLine": 5550 }, { "endCharacter": 93, - "endLine": 5533, + "endLine": 5551, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 49, - "startLine": 5533 + "startLine": 5551 }, { "endCharacter": 93, - "endLine": 5533, + "endLine": 5551, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 49, - "startLine": 5533 + "startLine": 5551 }, { "endCharacter": 89, - "endLine": 5534, + "endLine": 5552, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 47, - "startLine": 5534 + "startLine": 5552 }, { "endCharacter": 89, - "endLine": 5534, + "endLine": 5552, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\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 \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 47, - "startLine": 5534 + "startLine": 5552 }, { "endCharacter": 89, - "endLine": 5535, + "endLine": 5553, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 46, - "startLine": 5535 + "startLine": 5553 }, { "endCharacter": 89, - "endLine": 5535, + "endLine": 5553, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 46, - "startLine": 5535 + "startLine": 5553 }, { "endCharacter": 75, - "endLine": 5536, + "endLine": 5554, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 39, - "startLine": 5536 + "startLine": 5554 }, { "endCharacter": 75, - "endLine": 5536, + "endLine": 5554, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" 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": 39, - "startLine": 5536 + "startLine": 5554 }, { "endCharacter": 33, diff --git a/ReforceXY/README.md b/ReforceXY/README.md index 04208f4..017c55a 100644 --- a/ReforceXY/README.md +++ b/ReforceXY/README.md @@ -190,12 +190,13 @@ evaluation are outside this timeout. The supplied zero timeout disables the HPO timeout. Evaluation environments/episodes also cost resources; a larger budget is not evidence of better economics. -The objective maximizes the best mean deterministic evaluation **episode reward** -across periodic and final evaluations. Progress may display the latest mean -reward, which is not the selected best mean reward. The objective is not net -portfolio profit, Sharpe or Sortino. The holdout participates in model/HPO -selection; reserve a separate unseen market period for economic confirmation -under the [evaluation protocol](../docs/evaluation.md). +The objective maximizes the best mean deterministic evaluation **episode score** +across periodic and final evaluations. This score excludes PBRS shaping but +retains enabled non-PBRS additives. The same score selects checkpoints and +controls no-improvement stopping. Progress may display the latest mean, not the +selected best mean. The objective is not net portfolio profit, Sharpe or +Sortino. The holdout participates in model/HPO selection; reserve a separate +unseen market period under the [evaluation protocol](../docs/evaluation.md). Optuna trains fresh candidates in current-window feature coordinates. Sampled parameters override supplied model parameters during trials. Hyperband prunes @@ -234,12 +235,13 @@ Continual learning trains an independent copy of the deployed policy with its fitted feature pipeline. DQN/QRDQN deployments each persist their replay buffer; it is loaded only when continual training starts. Missing or incompatible replay data prevents continuation but does not prevent inference. The deployment -generation covers frozen feature coordinates and PBRS reward semantics. Otherwise -compatible older generations, including `chronological-frozen-pipelines-v2`, support -inference but cannot continue training. Reset trained models or use a new -`freqai.identifier` to migrate. Training disables `shuffle_after_split`. HPO -studies and saved best parameters are reused only when their objective identity -matches. +generation covers frozen feature coordinates and reward semantics, including +idle/hold costs on invalid actions. Otherwise compatible older generations, +including `chronological-frozen-pipelines-v2` and +`chronological-frozen-pipelines-pbrs-v3`, support inference but cannot continue +training. Reset trained models or use a new `freqai.identifier` to migrate. +Training disables `shuffle_after_split`. HPO studies and saved best parameters +are reused only when their objective identity matches. Backtests continue only from a saved policy whose training cutoff precedes the current window's end and its last available candle boundary. A later @@ -345,7 +347,7 @@ Moving base images can change them; record the runtime version. | freqai.rl_config.model_reward_parameters.rr | Schema: 1; direct constructor requires the member | Numeric reward-to-risk factor | 2 | The schema injects an omitted member into an existing reward map; direct environment construction without it raises. Native reward target uses rr × profit_aim. | | freqai.rl_config.model_reward_parameters.profit_aim | Schema: 0.025; direct constructor requires the member | Numeric profit target | 0.025 | The schema injects an omitted member into an existing reward map; direct environment construction without it raises. This reward target does not place a live take-profit order. | | freqai.rl_config.model_reward_parameters.max_trade_duration_candles | 128 | Integer-convertible value; no local range validation | 96 | Duration scale for reward penalties, not a live timeout. The schema accepts the top-level homonym `freqai.rl_config.max_trade_duration_candles`, but ReforceXY ignores it; only this reward-map member takes effect. | -| freqai.rl_config.model_reward_parameters.max_idle_duration_candles | 4 × effective max_trade_duration_candles | Integer-convertible value; no local range validation | Omitted (384 from the supplied trade scale) | Idle duration scale for reward penalties, not a live timer. | +| freqai.rl_config.model_reward_parameters.max_idle_duration_candles | 4 × effective max_trade_duration_candles | None or integer-convertible value; no local range validation | Omitted (384 from the supplied trade scale) | Missing/None derives the scale; explicit zero uses denominator 1. Reward scale, not a live timer. | ### HPO settings @@ -416,7 +418,9 @@ Rewards combine the fill-time base components with a potential-based shaping delta over the returned next observation. Termination liquidates any remaining position once and sets terminal shaping to `-prev_potential`, even for tiny nonzero potentials. `get_env_history()` returns one metrics/price row per transition. -Its `execution_tick` is the transition/action/fill +History `reward` remains the shaped training reward; `reward_selection` records +the separate evaluation contribution without PBRS. +The transition's `execution_tick` is the action/fill key before the tick increment; its `tick` is the returned post-increment price and observation row (normally `execution_tick + 1`). Exit-efficiency extrema include the fee-adjusted PnL at entry and subsequent retained market marks. Ordered diff --git a/ReforceXY/reward_space_analysis/README.md b/ReforceXY/reward_space_analysis/README.md index 018c9ce..3436c7d 100644 --- a/ReforceXY/reward_space_analysis/README.md +++ b/ReforceXY/reward_space_analysis/README.md @@ -269,9 +269,10 @@ artifacts are written. (Simulation-only). - **`--trading_mode`** (spot|margin|futures, default: spot) – spot: no shorts; margin/futures: shorts enabled. (Simulation-only). -- **`--max_duration_ratio`** (float, default: 2.5) – Upper multiple for sampled - trade durations (idle derived). (Simulation-only; not in reward params; cannot - be set via `--params`). +- **`--max_duration_ratio`** (float, default: 2.5) – Synthetic trade-duration + bound: `max(1, trunc(max_trade_duration_candles * max_duration_ratio))` + candles. At the bound, sampling chooses the valid exit with probability 1. + This is not a runtime exit timeout. (Simulation-only; not in `--params`). - **`--pnl_base_std`** (float, default: 0.02) – Base standard deviation for synthetic PnL generation (pre-scaling). (Simulation-only). - **`--pnl_duration_vol_scale`** (float, default: 0.5) – Additional PnL @@ -293,10 +294,11 @@ be overridden via `--params`. 0.03=3%). - **`--risk_reward_ratio`** (float, default: 2.0) – Risk-reward multiplier. - **`--action_masking`** (bare flag, default: true) – Disable via - `--params action_masking=false`. With masking enabled, sample - only valid actions. When disabled, sample an invalid action with 10% probability - and apply the configured invalid-action penalty. Invalid actions leave the held - position unchanged, except for an independent terminal liquidation. + `--params action_masking=false`. Masking samples only valid actions. Without + masking, invalid actions have 10% probability except at the forced duration + exit. They retain the current position's time cost and add the configured + invalid-action penalty. An independent terminal liquidation can still close + the position. ### Reward & Shaping @@ -338,8 +340,9 @@ 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 +fractional duration bounds use float-product truncation; extended bounds use the +supplied ratio's exact rational value. Both have a one-candle physical minimum. +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. @@ -384,6 +387,34 @@ the first named group (`pnl+`) has higher rewards than the second (`pnl-`). | `base_factor` | 100.0 | Base reward scale | | `invalid_action` | -2.0 | Penalty for invalid actions | +**Reward composition:** + +```text +R_base = invalid_penalty + idle_penalty + hold_penalty + exit_reward +R_train = R_base + reward_shaping + entry_additive + exit_additive +R_selection = R_base + entry_additive + exit_additive +``` + +The invalid penalty is `invalid_action` for an invalid unmasked action, and zero otherwise. + +Time costs use the pre-action duration when the action leaves the position +unchanged, including invalid actions. A valid entry has no idle cost; a valid +exit has no hold cost. `exit_reward = pnl * exit_factor` on a valid exit. +Episode termination also adds a liquidation exit reward and any enabled exit +additive at the next marked price if a position remains open. + +Training uses `R_train`. Runtime evaluation sums `R_selection` without discounting +for checkpoint selection, HPO and no-improvement stopping. The synthetic `reward` +column remains `R_train`; it is not the runtime selection score or portfolio +profit. Canonical mode suppresses both optional additives. + +For transition-faithful Python API calculations, supply the actual `next_context` +after time advances and before terminal liquidation. It is required, with the +resulting position's side, whenever `terminated=True` leaves a position open. +Missing or wrong-side terminal states raise `ValueError`. Without a nonterminal +`next_context`, the helper evaluates its local state approximation, not an +observed market transition. + #### Exit Factor The exit factor is computed as: @@ -391,6 +422,9 @@ The exit factor is computed as: `exit_factor` = `base_factor` · `pnl_target_coefficient` · `efficiency_coefficient` · `time_attenuation_coefficient` +Time attenuation reduces the magnitude of both profitable and losing exit +rewards. Holding costs are separate. + ##### PnL Target | Parameter | Default | Description | @@ -490,17 +524,35 @@ where `kernel_function` depends on `exit_attenuation_mode`. See #### Duration Penalties -| Parameter | Default | Description | -| ---------------------------- | ------- | ------------------------------------------------------------------------------------------- | -| `max_trade_duration_candles` | 128 | Trade duration cap | -| `max_idle_duration_candles` | None | Idle hazard threshold; missing/None derives 4× trade cap; idle clock keeps counting past it | -| `idle_penalty_ratio` | 1.0 | Idle penalty ratio | -| `idle_penalty_power` | 1.025 | Idle penalty exponent | -| `hold_penalty_ratio` | 1.0 | Hold penalty ratio | -| `hold_penalty_power` | 1.025 | Hold penalty exponent | +| Parameter | Default | Description | +| ---------------------------- | ------- | ----------------------------------------------------------------------------- | +| `max_trade_duration_candles` | 128 | Trade-duration scale and overtime threshold; not an exit timeout | +| `max_idle_duration_candles` | None | Idle scale; missing/None derives 4× the trade scale; the clock keeps counting | +| `idle_penalty_ratio` | 1.0 | Idle penalty ratio | +| `idle_penalty_power` | 1.025 | Idle penalty exponent | +| `hold_penalty_ratio` | 1.0 | Hold penalty ratio | +| `hold_penalty_power` | 1.025 | Hold penalty exponent | -An explicit zero threshold uses denominator 1 and does not disable the idle -penalty. +Let `B = base_factor`, `a = profit_aim`, `q = risk_reward_ratio`, +`D = max(1, max_trade_duration_candles)` and +`I = max(1, effective max_idle_duration_candles)`. The time-cost scale is +`C = B * a / q`, distinct from the PnL target `a * q`. + +For unchanged positions: + +```text +idle_penalty = -C * idle_penalty_ratio * (idle_duration / I)^idle_penalty_power + +hold_penalty = 0 if trade_duration / D < 1 + = -C * hold_penalty_ratio + * (trade_duration / D - 1)^hold_penalty_power otherwise +``` + +Idle applies while neutral; hold applies while long or short. Invalid no-op +actions keep these costs, even when `invalid_action=0`. Durations are candles, +not elapsed seconds. An explicit idle scale of zero uses denominator 1; it +does not disable the cost. With exponent zero, `0^0 = 1`, so the hold cost +already applies at its threshold. #### Validation @@ -572,9 +624,9 @@ where: - `m_dur = 1.0` if `r_pnl ≥ 0` (profit side) - `m_dur = risk_reward_ratio` if `r_pnl < 0` (loss side) -The loss-side duration multiplier (`m_dur = risk_reward_ratio`) scales the -duration penalty when holding losing positions, encouraging faster exits from -losses compared to symmetric treatment. +The loss-side multiplier changes the duration term of the potential, not the +base holding cost or optional additives. It does not establish faster exits +or better financial outcomes for a fitted policy. #### Entry Additive (Optional) @@ -586,6 +638,18 @@ losses compared to symmetric treatment. | `entry_additive_transform_pnl` | tanh | PnL transform | | `entry_additive_transform_duration` | tanh | Duration transform | +Both optional additives use the same signal, with `k = entry` or `exit`: + +```text +A_k(pnl, r_dur) = base_factor * k_additive_ratio * 0.5 + * [T_pnl(k_additive_gain * r_pnl) + + sign_eps(r_pnl) * T_dur(k_additive_gain * max(r_dur, 0))] +``` + +Use `r_pnl` and `sign_eps` from the hold-potential definition, and the selected +additive's transforms. There is no loss-side `risk_reward_ratio` multiplier. +Entry uses fee-aware PnL at the fill with `r_dur = 0`. + #### Exit Additive (Optional) | Parameter | Default | Description | @@ -596,6 +660,11 @@ losses compared to symmetric treatment. | `exit_additive_transform_pnl` | tanh | PnL transform | | `exit_additive_transform_duration` | tanh | Duration transform | +Exit uses PnL and trade duration at the exit fill, with +`r_dur = trade_duration / max(1, max_trade_duration_candles)`. A terminal +liquidation uses the actual next marked PnL and advanced duration instead. +Additives are zero when disabled and are always suppressed in canonical mode. + ### Exit Attenuation Kernels `r` = duration ratio and `grace` = `exit_plateau_grace`. @@ -764,8 +833,9 @@ the manifest is produced after reporting succeeds. The `sample_entry_prob`, `sample_exit_prob`, and `sample_neutral_prob` columns in `reward_samples.csv` report marginal probabilities of valid actions when applicable. -With masking disabled, these probabilities include the 90% valid-action mass; -they are not conditional on drawing a valid action. +With masking disabled, ordinary draws include the 90% valid-action mass; they +are not conditional on drawing a valid action. At the synthetic duration bound, +the corresponding valid exit has probability 1 and invalid actions are not drawn. ### Manifest (`manifest.json`) diff --git a/ReforceXY/reward_space_analysis/reward_space_analysis.py b/ReforceXY/reward_space_analysis/reward_space_analysis.py index be956b6..eec2d6b 100644 --- a/ReforceXY/reward_space_analysis/reward_space_analysis.py +++ b/ReforceXY/reward_space_analysis/reward_space_analysis.py @@ -929,7 +929,7 @@ def _compute_time_attenuation_coefficient( """ Calculate time-based attenuation coefficient using configurable strategy. - Returns a coefficient (typically in range [0.5, 2.0]) to multiply with base_factor. + Returns a coefficient in [0, 1] for validated parameters. """ if duration_ratio < 0.0: duration_ratio = 0.0 @@ -975,7 +975,7 @@ def _compute_time_attenuation_coefficient( stacklevel=2, ) alpha = 1.0 - return 1.0 / math.pow(1.0 + dr, alpha) + return math.pow(1.0 + dr, -alpha) def _half_life_kernel(dr: float) -> float: hl = _get_float_param(params, "exit_half_life") @@ -1101,7 +1101,9 @@ def _compute_pnl_target_coefficient( """ Compute PnL target coefficient based on PnL/target ratio using tanh. - Returns a coefficient (typically 0.5-2.0) to be multiplied with base_factor. + For nonnegative tunables, amplification ranges from 1 to ``1 + win_reward_factor`` + for gains, or ``1 + win_reward_factor * rr`` for losses. Here ``rr`` is the positive + ``risk_reward_ratio``, or ``RISK_REWARD_RATIO_DEFAULT`` otherwise. The coefficient rewards trades that exceed pnl_target and penalizes losses beyond the risk/reward threshold. @@ -1112,7 +1114,7 @@ def _compute_pnl_target_coefficient( risk_reward_ratio: Risk/reward ratio for loss penalty calculation Returns: - float: Coefficient ≥ 0.0 (typically 0.5-2.0 range) + float: Nonnegative amplification coefficient for nonnegative tunables. """ pnl_target_coefficient = 1.0 @@ -1342,6 +1344,11 @@ def calculate_reward( Whether to apply action masking (affects invalid action penalty) prev_potential : float, optional Previous state potential Φ(s), by default np.nan + next_context : RewardContext, optional + Actual marked state after advancing time, before terminal liquidation. + Required when a terminal transition leaves a position open. + terminated : bool + Close any resulting open position and clear the terminal potential. Returns ------- @@ -1371,10 +1378,9 @@ def calculate_reward( short_allowed=short_allowed, ) - base_reward: float | None = None + base_reward = 0.0 if not is_valid and not action_masking: breakdown.invalid_penalty = _get_float_param(params, "invalid_action") - base_reward = breakdown.invalid_penalty base_factor = _get_float_param(params, "base_factor", base_factor) @@ -1396,35 +1402,31 @@ def calculate_reward( context.trade_duration, max_trade_duration_candles ) - # Base reward calculation - if base_reward is None: - if context.action == Actions.Neutral: - if context.position == Positions.Neutral: - base_reward = _idle_penalty(context, idle_factor, params) - breakdown.idle_penalty = base_reward - elif context.position in (Positions.Long, Positions.Short): - base_reward = _hold_penalty(context, hold_factor, params) - breakdown.hold_penalty = base_reward - else: - base_reward = 0.0 - else: - is_exit_action = ( - context.action == Actions.Long_exit and context.position == Positions.Long - ) or (context.action == Actions.Short_exit and context.position == Positions.Short) - if is_exit_action: - base_reward = _compute_exit_reward( - base_factor, - pnl_target, - current_duration_ratio, - context, - params, - risk_reward_ratio, - ) - breakdown.exit_component = base_reward - breakdown.exit_pnl = context.current_pnl - else: - base_reward = 0.0 + # Time advances even when an invalid action leaves the physical state unchanged. + if context.action == Actions.Neutral or not is_valid: + if context.position == Positions.Neutral: + base_reward = _idle_penalty(context, idle_factor, params) + breakdown.idle_penalty = base_reward + elif context.position in (Positions.Long, Positions.Short): + base_reward = _hold_penalty(context, hold_factor, params) + breakdown.hold_penalty = base_reward + else: + is_exit_action = ( + context.action == Actions.Long_exit and context.position == Positions.Long + ) or (context.action == Actions.Short_exit and context.position == Positions.Short) + if is_exit_action: + base_reward = _compute_exit_reward( + base_factor, + pnl_target, + current_duration_ratio, + context, + params, + risk_reward_ratio, + ) + breakdown.exit_component = base_reward + breakdown.exit_pnl = context.current_pnl + base_reward += breakdown.invalid_penalty breakdown.base_reward = float(base_reward) # === PBRS INTEGRATION === @@ -1486,23 +1488,26 @@ def calculate_reward( next_duration_ratio = _compute_duration_ratio( next_context.trade_duration, max_trade_duration_candles ) - terminal_context = next_context if next_context is not None else context breakdown.terminal_liquidation = bool( terminated and next_position in (Positions.Long, Positions.Short) ) if breakdown.terminal_liquidation: + if next_context is None or next_context.position != next_position: + raise ValueError( + "next_context with the resulting open position is required for terminal liquidation" + ) liquidation_reward = _compute_exit_reward( base_factor, pnl_target, next_duration_ratio, - terminal_context, + next_context, params, risk_reward_ratio, ) base_reward += liquidation_reward breakdown.base_reward = float(base_reward) breakdown.exit_component += liquidation_reward - breakdown.exit_pnl = terminal_context.current_pnl + breakdown.exit_pnl = next_context.current_pnl # Resolve effective PBRS and additive enablement for every transition. exit_mode = _resolve_exit_potential_mode( @@ -1512,11 +1517,10 @@ def calculate_reward( ) hold_potential_enabled = _get_bool_param(params, "hold_potential_enabled") - entry_additive_enabled = ( - False if exit_mode == "canonical" else _get_bool_param(params, "entry_additive_enabled") - ) - exit_additive_enabled = ( - False if exit_mode == "canonical" else _get_bool_param(params, "exit_additive_enabled") + entry_additive_enabled, exit_additive_enabled, _ = _resolve_additive_enablement( + exit_mode, + _get_bool_param(params, "entry_additive_enabled"), + _get_bool_param(params, "exit_additive_enabled"), ) pbrs_enabled = bool(hold_potential_enabled or entry_additive_enabled or exit_additive_enabled) @@ -1558,7 +1562,7 @@ def calculate_reward( ) if breakdown.terminal_liquidation and exit_additive_enabled: exit_additive += _compute_exit_additive( - terminal_context.current_pnl, pnl_target, next_duration_ratio, params, base_factor + next_pnl, pnl_target, next_duration_ratio, params, base_factor ) if terminated: next_potential = 0.0 @@ -1669,7 +1673,16 @@ def _sample_action( idle_duration: int, max_idle_duration_candles: int, action_masking: bool = True, + max_trade_duration_cap: int | None = None, ) -> tuple[Actions, float, float, float]: + if ( + max_trade_duration_cap is not None + and position in (Positions.Long, Positions.Short) + and trade_duration >= max_trade_duration_cap + ): + action = Actions.Long_exit if position == Positions.Long else Actions.Short_exit + return action, float("nan"), 1.0, float("nan") + entry_prob, exit_prob, neutral_prob = _sampling_probabilities( position, short_allowed=short_allowed, @@ -1813,7 +1826,7 @@ def simulate_samples( max_idle_duration_candles = get_max_idle_duration_candles( params, max_trade_duration_candles=max_trade_duration_candles ) - max_trade_duration_cap = _scale_duration(max_trade_duration_candles, max_duration_ratio) + max_trade_duration_cap = max(1, _scale_duration(max_trade_duration_candles, max_duration_ratio)) samples: list[dict[str, float | None]] = [] prev_potential: float = 0.0 @@ -1842,6 +1855,7 @@ def simulate_samples( idle_duration=idle_duration, max_idle_duration_candles=max_idle_duration_candles, action_masking=action_masking, + max_trade_duration_cap=max_trade_duration_cap, ) context = RewardContext( @@ -1875,7 +1889,7 @@ def simulate_samples( idle_duration = 0 entry_open = current_open if position in (Positions.Long, Positions.Short): - trade_duration = min(trade_duration + 1, max_trade_duration_cap) + trade_duration += 1 else: idle_duration += 1 # Simulate synthetic open-price movement. @@ -2064,8 +2078,12 @@ def _validate_simulation_invariants(df: pd.DataFrame, params: RewardParams) -> N # INVARIANT 1: masked sampling must only emit legal actions; unmasked # actions may be invalid but cannot 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)] + long_exits = df[ + (df["action"] == Actions.Long_exit.value) & (df["position"] != Positions.Long.value) + ] + short_exits = df[ + (df["action"] == Actions.Short_exit.value) & (df["position"] != Positions.Short.value) + ] if len(long_exits) > 0: raise AssertionError(f"Sim: {len(long_exits)} Long_exit actions without Long position") if len(short_exits) > 0: @@ -2073,8 +2091,13 @@ def _validate_simulation_invariants(df: pd.DataFrame, params: RewardParams) -> N f"Sim: {len(short_exits)} Short_exit actions without Short position" ) - long_entries = df[(df["action"] == 1.0) & (df["position"] != 0.5)] - short_entries = df[(df["action"] == 3.0) & (df["position"] != 0.5)] + long_entries = df[ + (df["action"] == Actions.Long_enter.value) & (df["position"] != Positions.Neutral.value) + ] + short_entries = df[ + (df["action"] == Actions.Short_enter.value) + & (df["position"] != Positions.Neutral.value) + ] if len(long_entries) > 0: raise AssertionError( f"Sim: {len(long_entries)} Long_enter actions without Neutral position" @@ -2093,20 +2116,22 @@ def _validate_simulation_invariants(df: pd.DataFrame, params: RewardParams) -> N 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)] + neutral_with_trade = df[ + (df["position"] == Positions.Neutral.value) & (df["trade_duration"] > 0) + ] if len(neutral_with_trade) > 0: raise AssertionError( f"Sim: {len(neutral_with_trade)} Neutral positions with non-zero trade_duration" ) - inpos_with_idle = df[(df["position"] != 0.5) & (df["idle_duration"] > 0)] + inpos_with_idle = df[(df["position"] != Positions.Neutral.value) & (df["idle_duration"] > 0)] if len(inpos_with_idle) > 0: raise AssertionError( f"Sim: {len(inpos_with_idle)} in-position samples with idle_duration > 0" ) # INVARIANT 3: Neutral states have zero PnL (simulation design) - neutral_with_pnl = df[(df["position"] == 0.5) & (df["pnl"].abs() > eps_pnl)] + neutral_with_pnl = df[(df["position"] == Positions.Neutral.value) & (df["pnl"].abs() > eps_pnl)] if len(neutral_with_pnl) > 0: raise AssertionError(f"Sim: {len(neutral_with_pnl)} Neutral positions with non-zero pnl") @@ -2330,7 +2355,7 @@ def _compute_representativity_stats( pnl_target = _resolve_pnl_target(profit_aim, risk_reward_ratio) total = len(df) # Map numeric position codes to readable labels to avoid casting Neutral (0.5) to 0 - pos_label_map = {0.0: "Short", 0.5: "Neutral", 1.0: "Long"} + pos_label_map = {position.value: position.name for position in Positions} pos_labeled = df["position"].map(pos_label_map) pos_counts = ( pos_labeled.value_counts().reindex(["Short", "Neutral", "Long"]).fillna(0).astype(int) @@ -3925,7 +3950,7 @@ def build_argument_parser() -> argparse.ArgumentParser: "--max_duration_ratio", type=float, default=2.5, - help="Multiple of max duration used when sampling trade/idle durations.", + help="Synthetic trade-duration bound multiplier; positions exit at the bound (minimum one candle).", ) parser.add_argument( "--pnl_base_std", @@ -4808,8 +4833,7 @@ def write_complete_statistical_analysis( f.write("| Feature | KL Div | JS Dist | Wasserstein | KS Stat | KS p-value |\n") f.write("|---------|--------|---------|-------------|---------|------------|\n") - features = ["pnl", "trade_duration", "idle_duration"] - for feature in features: + for feature in _DISTRIBUTION_SHIFT_FEATURES: kl = distribution_shift.get(f"{feature}_kl_divergence", np.nan) js = distribution_shift.get(f"{feature}_js_distance", np.nan) ws = distribution_shift.get(f"{feature}_wasserstein", np.nan) diff --git a/ReforceXY/reward_space_analysis/tests/README.md b/ReforceXY/reward_space_analysis/tests/README.md index 0197533..f9d2290 100644 --- a/ReforceXY/reward_space_analysis/tests/README.md +++ b/ReforceXY/reward_space_analysis/tests/README.md @@ -188,7 +188,7 @@ Columns: | 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-decomposition-integrity-101 | robustness | Selected scenarios isolate idle, hold, exit and pre-overtime invalid costs | robustness/test_robustness.py::test_decomposition_integrity | Invalid overtime costs are covered by components-invalid-time-costs-160 | | 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 | | @@ -231,20 +231,22 @@ Columns: | 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 | | statistics-report-inference-projection-146 | statistics | Constant confidence intervals require declared independence, even with an empty hypothesis map | statistics/test_statistics.py::test_report_projects_constant_intervals_only_with_declared_independence | Numeric projection; True → False → default rewrites the same report; non-owning bounds reference: statistics-constant-dist-exact-ci-113a | | statistics-report-feature-availability-147 | statistics | Complete importance exports require a fitted model and finite means and standard deviations | statistics/test_statistics.py::test_report_certifies_feature_importance_only_from_finite_estimates | Real finite-zero, single-feature, empty-feature and one-test-row states; numerical CSV and availability. | - -| 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-150 | 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-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 | +| 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-150 | 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-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-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 | +| components-invalid-time-costs-160 | components | Invalid no-ops retain the current state's idle or hold cost | components/test_reward_components.py::test_invalid_actions_add_to_the_unchanged_states_time_cost | Neutral, long and short states | +| components-power-subnormal-161 | components | Power attenuation retains representable subnormals and underflows without changing kernels | components/test_reward_components.py::test_power_kernel_retains_subnormal_values_and_true_underflow | No absolute tolerance masking | +| pbrs-physical-duration-bound-162 | pbrs | Synthetic duration bounds produce valid exits without freezing the physical clock | pbrs/test_pbrs.py::test_synthetic_duration_cap_closes_trades_without_freezing_the_clock | Includes fractional one-candle minimum, unmasked sampling and terminal PBRS | +| pbrs-terminal-next-state-163 | pbrs | Terminal liquidation requires the actual marked next state | pbrs/test_pbrs.py::test_terminal_liquidation_requires_the_actual_marked_next_state | Both sides, fees, missing and wrong-side states | ### Non-Owning Smoke / Reference Checks diff --git a/ReforceXY/reward_space_analysis/tests/api/test_api_helpers.py b/ReforceXY/reward_space_analysis/tests/api/test_api_helpers.py index 2318d34..3308c82 100644 --- a/ReforceXY/reward_space_analysis/tests/api/test_api_helpers.py +++ b/ReforceXY/reward_space_analysis/tests/api/test_api_helpers.py @@ -98,19 +98,6 @@ class TestAPIAndHelpers(RewardSpaceTestBase): 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 @@ -334,8 +321,7 @@ class TestAPIAndHelpers(RewardSpaceTestBase): values = ( df[["sample_entry_prob", "sample_exit_prob", "sample_neutral_prob"]].stack().dropna() ) - prob_upper_bound = SCENARIOS.API_PROBABILITY_UPPER_BOUND - self.assertTrue(((values >= 0.0) & (values <= prob_upper_bound)).all()) + self.assertTrue(((values >= 0.0) & (values <= 1.0)).all()) def test_unmasked_simulation_samples_invalid_actions_without_changing_position(self): """Unmasked actions expose penalties without inventing trades or wrong-side exits.""" @@ -354,7 +340,15 @@ class TestAPIAndHelpers(RewardSpaceTestBase): self.assertGreater(len(invalid), 0) self.assertLess(len(invalid), len(df)) self.assertTrue((invalid["reward_invalid"] == params["invalid_action"]).all()) - self.assertTrue((invalid["reward_base"] == params["invalid_action"]).all()) + np.testing.assert_allclose( + invalid["reward_base"], + invalid["reward_invalid"] + + invalid["reward_idle"] + + invalid["reward_hold"] + + invalid["reward_exit"], + atol=TOLERANCE.IDENTITY_STRICT, + rtol=0.0, + ) wrong_exit = invalid.loc[ ( ( @@ -616,11 +610,12 @@ class TestPrivateFunctions(RewardSpaceTestBase): self.assertAlmostEqualFloat( breakdown.total, breakdown.invalid_penalty + + breakdown.hold_penalty + breakdown.reward_shaping + breakdown.entry_additive + breakdown.exit_additive, tolerance=TOLERANCE.IDENTITY_RELAXED, - msg="Total should equal invalid penalty plus shaping/additives", + msg="Invalid actions retain the current position's time cost", ) def test_new_invariant_and_warn_parameters(self): diff --git a/ReforceXY/reward_space_analysis/tests/components/test_reward_components.py b/ReforceXY/reward_space_analysis/tests/components/test_reward_components.py index 851b79a..97a1016 100644 --- a/ReforceXY/reward_space_analysis/tests/components/test_reward_components.py +++ b/ReforceXY/reward_space_analysis/tests/components/test_reward_components.py @@ -13,6 +13,7 @@ from reward_space_analysis import ( _compute_efficiency_coefficient, _compute_hold_potential, _compute_pnl_target_coefficient, + _compute_time_attenuation_coefficient, _get_exit_factor, _get_float_param, get_max_idle_duration_candles, @@ -39,6 +40,59 @@ pytestmark = pytest.mark.components class TestRewardComponents(RewardSpaceTestBase): + def test_invalid_actions_add_to_the_unchanged_states_time_cost(self): + """Retain the physical time cost when an invalid action cannot trade.""" + params = self.base_params(hold_potential_enabled=False) + trade_duration = 3 * params["max_trade_duration_candles"] + idle_duration = 3 * get_max_idle_duration_candles(params) + for position, invalid_action in ( + (Positions.Neutral, Actions.Long_exit), + (Positions.Long, Actions.Long_enter), + (Positions.Short, Actions.Short_enter), + ): + with self.subTest(position=position): + common = { + "pnl": 0.0, + "trade_duration": trade_duration, + "idle_duration": idle_duration, + "max_unrealized_profit": 0.0, + "min_unrealized_profit": 0.0, + "position": position, + } + valid = calculate_reward_with_defaults( + self.make_ctx(**common, action=Actions.Neutral), params, action_masking=False + ) + invalid = calculate_reward_with_defaults( + self.make_ctx(**common, action=invalid_action), params, action_masking=False + ) + self.assertLess(valid.base_reward, params["invalid_action"]) + self.assertAlmostEqualFloat( + invalid.base_reward, + valid.base_reward + params["invalid_action"], + tolerance=TOLERANCE.IDENTITY_STRICT, + ) + self.assertEqual(invalid.idle_penalty, valid.idle_penalty) + self.assertEqual(invalid.hold_penalty, valid.hold_penalty) + + def test_power_kernel_retains_subnormal_values_and_true_underflow(self): + """Avoid intermediate overflow without substituting a different kernel.""" + params = self.base_params( + exit_attenuation_mode="power", + exit_plateau=False, + exit_power_tau=SCENARIOS.POWER_SUBNORMAL_TAU, + ) + ratio = SCENARIOS.POWER_SUBNORMAL_DURATION + expected = math.exp( + math.log(SCENARIOS.POWER_SUBNORMAL_TAU) / math.log(2) * math.log1p(ratio) + ) + actual = _compute_time_attenuation_coefficient(ratio, params) + self.assertGreater(actual, 0.0) + # Absolute tolerances would accept the erroneous linear fallback. + self.assertTrue(math.isclose(actual, expected, rel_tol=TOLERANCE.RELATIVE, abs_tol=0.0)) + self.assertEqual( + _compute_time_attenuation_coefficient(SCENARIOS.POWER_UNDERFLOW_DURATION, params), 0.0 + ) + def test_hold_potential_computation_finite(self): """Test hold potential computation returns finite values.""" params = { diff --git a/ReforceXY/reward_space_analysis/tests/constants.py b/ReforceXY/reward_space_analysis/tests/constants.py index d2e704a..c6bccf3 100644 --- a/ReforceXY/reward_space_analysis/tests/constants.py +++ b/ReforceXY/reward_space_analysis/tests/constants.py @@ -336,8 +336,14 @@ class TestScenarios: API_ENTRY_RATE_DRAWS: Draw count for entry-rate estimation (2000) API_MAX_TRADE_DURATION_CANDLES: Max trade duration used in API simulation tests (40) API_MAX_DURATION_RATIO: Max duration ratio used in API simulation tests (1.5) - API_PROBABILITY_UPPER_BOUND: Upper bound for exposed sampling probabilities (0.9) API_EXTREME_BASE_FACTOR: Extreme base_factor used to trigger warning paths (10000000.0) + CLOCK_TRADE_SCALE: One-candle reward scale exposing the synthetic clock boundary + CLOCK_MIN_DURATION_RATIO: Fractional cap requiring a one-candle physical minimum (0.5) + CLOCK_DURATION_RATIO: Two-candle synthetic cap after ordinary truncation (2.5) + TERMINAL_ENTRY_FEE: Symmetric fee used by terminal liquidation oracles (0.001) + POWER_SUBNORMAL_TAU: Strictly accepted power-kernel tau (1e-6) + POWER_SUBNORMAL_DURATION: Ratio with a representable subnormal result (1e16) + POWER_UNDERFLOW_DURATION: Ratio whose power-kernel result underflows to zero (1e18) # CLI smoke parameters CLI_NUM_SAMPLES_STANDARD: Default CLI sample size for smoke runs (200) @@ -375,8 +381,14 @@ class TestScenarios: API_ENTRY_RATE_DRAWS: int = 2000 API_MAX_TRADE_DURATION_CANDLES: int = 40 API_MAX_DURATION_RATIO: float = 1.5 - API_PROBABILITY_UPPER_BOUND: float = 0.9 API_EXTREME_BASE_FACTOR: float = 10_000_000.0 + CLOCK_TRADE_SCALE: int = 1 + CLOCK_MIN_DURATION_RATIO: float = 0.5 + CLOCK_DURATION_RATIO: float = 2.5 + TERMINAL_ENTRY_FEE: float = 0.001 + POWER_SUBNORMAL_TAU: float = 1e-6 + POWER_SUBNORMAL_DURATION: float = 1e16 + POWER_UNDERFLOW_DURATION: float = 1e18 # CLI smoke parameters CLI_NUM_SAMPLES_STANDARD: int = 200 diff --git a/ReforceXY/reward_space_analysis/tests/pbrs/test_pbrs.py b/ReforceXY/reward_space_analysis/tests/pbrs/test_pbrs.py index 0366119..89c6913 100644 --- a/ReforceXY/reward_space_analysis/tests/pbrs/test_pbrs.py +++ b/ReforceXY/reward_space_analysis/tests/pbrs/test_pbrs.py @@ -50,6 +50,115 @@ pytestmark = pytest.mark.pbrs class TestSimulationParity(RewardSpaceTestBase): """Synthetic durations and reports follow runtime transition semantics.""" + def test_synthetic_duration_cap_closes_trades_without_freezing_the_clock(self): + """Bound physical trade duration with valid exits and truthful probabilities.""" + scale = SCENARIOS.CLOCK_TRADE_SCALE + params = self.base_params( + max_trade_duration_candles=scale, + action_masking=False, + hold_potential_enabled=True, + exit_potential_mode="canonical", + ) + for ratio in (SCENARIOS.CLOCK_MIN_DURATION_RATIO, SCENARIOS.CLOCK_DURATION_RATIO): + with self.subTest(ratio=ratio): + cap = max(1, int(scale * ratio)) + df = simulate_samples( + params=params, + num_samples=SCENARIOS.SAMPLE_SIZE_LARGE, + seed=SEEDS.BASE, + base_factor=PARAMS.BASE_FACTOR, + profit_aim=PARAMS.PROFIT_AIM, + risk_reward_ratio=PARAMS.RISK_REWARD_RATIO, + max_duration_ratio=ratio, + trading_mode="futures", + pnl_base_std=PARAMS.PNL_STD, + pnl_duration_vol_scale=PARAMS.PNL_DUR_VOL_SCALE, + ) + age = forced_exits = 0 + for row in df.itertuples(): + self.assertEqual(row.trade_duration, age) + if row.position != Positions.Neutral.value and age == cap: + expected = ( + Actions.Long_exit + if row.position == Positions.Long.value + else Actions.Short_exit + ) + self.assertEqual(row.action, expected.value) + self.assertEqual(row.sample_exit_prob, 1.0) + self.assertEqual(row.is_invalid, 0.0) + self.assertEqual(row.next_position, Positions.Neutral.value) + forced_exits += 1 + if row.next_position == Positions.Neutral.value: + age = 0 + elif row.position == Positions.Neutral.value: + age = 1 + else: + age += 1 + self.assertEqual(row.next_trade_duration, age) + self.assertLessEqual(age, cap) + self.assertGreater(forced_exits, 0) + verification = reward_space_analysis.verify_pbrs_trajectory( + df, float(params["potential_gamma"]) + ) + self.assertTrue(verification["verified"], verification) + + def test_terminal_liquidation_requires_the_actual_marked_next_state(self): + """Reject absent/wrong-side states and retain fee-aware terminal entry losses.""" + fee = SCENARIOS.TERMINAL_ENTRY_FEE + params = self.base_params( + base_factor=PARAMS.BASE_FACTOR, + entry_fee_rate=fee, + exit_fee_rate=fee, + hold_potential_enabled=False, + efficiency_weight=0.0, + ) + for side, action, pnl in ( + (Positions.Long, Actions.Long_enter, 1 / (1 + fee) ** 2 - 1), + (Positions.Short, Actions.Short_enter, 1 - (1 + fee) ** 2), + ): + with self.subTest(side=side): + context = self.make_ctx( + pnl=0.0, + trade_duration=0, + idle_duration=0, + max_unrealized_profit=0.0, + min_unrealized_profit=0.0, + position=Positions.Neutral, + action=action, + ) + with self.assertRaisesRegex(ValueError, "next_context"): + calculate_reward_with_defaults(context, params, terminated=True) + wrong = self.make_ctx( + pnl=0.0, + trade_duration=0, + idle_duration=0, + max_unrealized_profit=0.0, + min_unrealized_profit=0.0, + position=Positions.Neutral, + action=Actions.Neutral, + ) + with self.assertRaisesRegex(ValueError, "next_context"): + calculate_reward_with_defaults( + context, params, terminated=True, next_context=wrong + ) + next_context = self.make_ctx( + pnl=pnl, + trade_duration=1, + idle_duration=0, + max_unrealized_profit=pnl, + min_unrealized_profit=pnl, + position=side, + action=Actions.Neutral, + ) + result = calculate_reward_with_defaults( + context, params, terminated=True, next_context=next_context + ) + self.assertTrue(result.terminal_liquidation) + self.assertAlmostEqualFloat( + result.total, PARAMS.BASE_FACTOR * pnl, tolerance=TOLERANCE.IDENTITY_STRICT + ) + self.assertEqual(result.exit_pnl, pnl) + def test_neutral_terminal_closes_stored_potential(self): context = reward_space_analysis.RewardContext( current_pnl=0.0, diff --git a/ReforceXY/reward_space_analysis/tests/robustness/test_robustness.py b/ReforceXY/reward_space_analysis/tests/robustness/test_robustness.py index 149b2e5..f56b016 100644 --- a/ReforceXY/reward_space_analysis/tests/robustness/test_robustness.py +++ b/ReforceXY/reward_space_analysis/tests/robustness/test_robustness.py @@ -41,7 +41,7 @@ class TestRewardRobustnessAndBoundaries(RewardSpaceTestBase): # Owns invariant: robustness-decomposition-integrity-101 (robustness category) def test_decomposition_integrity(self): - """reward must equal the single active core component under mutually exclusive scenarios (idle/hold/exit/invalid).""" + """Isolate idle, hold, exit and pre-overtime invalid costs in selected scenarios.""" scenarios = [ { "ctx": self.make_ctx( diff --git a/ReforceXY/tests/test_model_optuna_lifecycle.py b/ReforceXY/tests/test_model_optuna_lifecycle.py index 930917d..c78f10e 100644 --- a/ReforceXY/tests/test_model_optuna_lifecycle.py +++ b/ReforceXY/tests/test_model_optuna_lifecycle.py @@ -139,7 +139,11 @@ class OptunaLifecycleTest(QaTestCase): self.assertNotEqual(result["gamma"], 0.91) def test_incompatible_or_missing_study_identity_discards_old_trials(self): - for identity in (None, "terminal-liquidation-risk-normalized-trained-policy-v3"): + for identity in ( + None, + "terminal-liquidation-risk-normalized-trained-policy-v3", + "terminal-liquidation-risk-normalized-trained-policy-v5", + ): with self.subTest(identity=identity): model = self.model() study = optuna.create_study( diff --git a/ReforceXY/tests/test_model_optuna_storage.py b/ReforceXY/tests/test_model_optuna_storage.py index 45e97f6..41a648c 100644 --- a/ReforceXY/tests/test_model_optuna_storage.py +++ b/ReforceXY/tests/test_model_optuna_storage.py @@ -207,6 +207,10 @@ class OptunaStorageTest(QaTestCase): {}, {"params": {"gamma": 0.1}}, {"objective_identity": "legacy", "params": {"gamma": 0.1}}, + { + "objective_identity": "terminal-liquidation-risk-normalized-trained-policy-v5", + "params": {"gamma": 0.1}, + }, {"objective_identity": ReforceXY._OPTUNA_OBJECTIVE_IDENTITY, "params": []}, ): with self.subTest(payload=payload): diff --git a/ReforceXY/tests/test_model_reward_contracts.py b/ReforceXY/tests/test_model_reward_contracts.py new file mode 100644 index 0000000..46f9302 --- /dev/null +++ b/ReforceXY/tests/test_model_reward_contracts.py @@ -0,0 +1,297 @@ +"""Reward costs, numerical boundaries and the native model-selection objective.""" + +import math +import tempfile +from types import SimpleNamespace + +import numpy as np +import pandas as pd +from qa_support import PAIR, QaTestCase, model_config +from sb3_contrib.common.maskable.evaluation import evaluate_policy + +from ReforceXY.user_data.freqaimodels.ReforceXY import ( + Actions, + MyRLEnv, + Positions, + ReforceXY, +) + + +class _TradeOrWaitPolicy: + """Choose an observable long round trip, or remain in cash.""" + + def __init__(self, trade): + self.trade = trade + + def predict(self, observation, state=None, **kwargs): + positions = observation[:, -1, -2] + actions = ( + np.where( + positions == Positions.Neutral.value, + Actions.Long_enter.value, + Actions.Long_exit.value, + ) + if self.trade + else np.full(len(observation), Actions.Neutral.value) + ) + return actions, state + + +class RewardContractsTest(QaTestCase): + def env(self, *, opens=None, **parameters): + prices = pd.DataFrame({"open": [100.0] * 16 if opens is None else opens}) + env = MyRLEnv( + df=prices.copy(), + prices=prices, + df_raw=prices.copy(), + window_size=1, + reward_kwargs={"rr": 2.0, "profit_aim": 0.03}, + fee=0.0, + can_short=True, + config={ + "stake_amount": "unlimited", + "freqai": { + "rl_config": { + "add_state_info": True, + "max_training_drawdown_pct": 0.99, + "model_reward_parameters": { + "max_trade_duration_candles": 1, + "max_idle_duration_candles": 1, + "hold_potential_enabled": False, + **parameters, + }, + } + }, + }, + live=True, + ) + self.addCleanup(env.close) + env.reset(seed=42) + return env + + def test_invalid_noop_keeps_time_cost_for_every_position(self): + for entry, invalid in ( + (None, Actions.Long_exit), + (Actions.Long_enter, Actions.Long_enter), + (Actions.Short_enter, Actions.Short_enter), + ): + for penalty in (0.0, -2.0): + with self.subTest(entry=entry, penalty=penalty): + valid = self.env(invalid_action=penalty) + invalid_env = self.env(invalid_action=penalty) + for env in (valid, invalid_env): + if entry is not None: + env.step(entry.value) + for _ in range(3): + env.step(Actions.Neutral.value) + obs, reward, _, _, info = valid.step(Actions.Neutral.value) + bad_obs, bad_reward, _, _, bad_info = invalid_env.step(invalid.value) + np.testing.assert_array_equal(bad_obs, obs) + self.assertLess(reward, 0.0) + self.assertAlmostEqual(bad_reward, reward + penalty) + self.assertEqual(bad_info["reward_idle"], info["reward_idle"]) + self.assertEqual(bad_info["reward_hold"], info["reward_hold"]) + self.assertEqual(invalid_env._total_profit, valid._total_profit) + self.assertEqual(len(invalid_env.trade_history), len(valid.trade_history)) + + def test_valid_entry_and_exit_do_not_pay_noop_costs(self): + env = self.env() + for _ in range(3): + env.step(Actions.Neutral.value) + _, reward, _, _, info = env.step(Actions.Long_enter.value) + self.assertEqual(reward, 0.0) + self.assertEqual(info["reward_idle"], 0.0) + for _ in range(3): + env.step(Actions.Neutral.value) + _, reward, _, _, info = env.step(Actions.Long_exit.value) + self.assertEqual(reward, 0.0) + self.assertEqual(info["reward_hold"], 0.0) + + def test_null_idle_scale_derives_trade_scale_but_zero_remains_explicit(self): + for idle_scale, denominator in ((None, 8), (0, 1)): + with self.subTest(idle_scale=idle_scale): + env = self.env(max_trade_duration_candles=2, max_idle_duration_candles=idle_scale) + for _ in range(3): + env.step(Actions.Neutral.value) + _, reward, _, _, _ = env.step(Actions.Neutral.value) + self.assertAlmostEqual(reward, -1.5 * (3 / denominator) ** 1.025) + + def test_power_exit_preserves_python_and_numpy_tau(self): + for tau in (0.25, np.float32(0.25), np.longdouble(0.25), np.int64(1)): + with self.subTest(tau_type=type(tau)): + env = self.env( + opens=[100.0, 100.0, 105.0, 105.0], + exit_attenuation_mode="power", + exit_plateau=False, + exit_power_tau=tau, + ) + env.step(Actions.Long_enter.value) + _, reward, terminated, _, _ = env.step(Actions.Long_exit.value) + self.assertTrue(terminated) + # At duration ratio one, K(1)=tau; the fee-free exit has base reward 7.5. + self.assertAlmostEqual(reward, 7.5 * float(tau)) + + def test_power_underflow_does_not_switch_to_a_linear_kernel(self): + env = self.env() + params = {"exit_attenuation_mode": "power", "exit_plateau": False, "exit_power_tau": 1e-6} + ratio = 1e16 + expected = math.exp(math.log(1e-6) / math.log(2) * math.log1p(ratio)) + actual = env._compute_time_attenuation_coefficient(ratio, params) + self.assertGreater(actual, 0.0) + # Subnormal rounding needs relative tolerance; an absolute epsilon would hide the defect. + self.assertTrue(math.isclose(actual, expected, rel_tol=1e-4, abs_tol=0.0)) + self.assertEqual(env._compute_time_attenuation_coefficient(1e18, params), 0.0) + + def test_native_evaluation_preserves_base_ranking_without_losing_training_shaping(self): + for ratio in (0.4, 1e16): + for stacking in (0, 2): + with ( + self.subTest(ratio=ratio, stacking=stacking), + tempfile.TemporaryDirectory() as temp, + ): + config = model_config( + temp, + freqai={ + "model_training_parameters": {"gamma": 0.8}, + "rl_config": { + "add_state_info": True, + "frame_stacking": stacking, + "model_reward_parameters": { + "profit_aim": 0.025, + "hold_potential_enabled": True, + "hold_potential_ratio": ratio, + "max_trade_duration_candles": 1, + "hold_potential_transform_pnl": "clip", + "hold_potential_transform_duration": "clip", + "idle_penalty_ratio": 0.0, + }, + }, + }, + ) + model = ReforceXY(config=config) + model.live = True + self.addCleanup(model.close_envs) + prices = pd.DataFrame({"open": [100.0, 100.0, 105.0, 105.0]}) + # Use the real factory and VecMonitor, not a replacement evaluation implementation. + model.set_train_and_eval_environments( + {"train_features": prices.copy(), "test_features": prices.copy()}, + prices, + prices, + SimpleNamespace(pair=PAIR), + ) + model.train_env.set_attr("fee", 0.0) + model.eval_env.set_attr("fee", 0.0) + trade_score, _ = evaluate_policy( + _TradeOrWaitPolicy(True), model.eval_env, n_eval_episodes=1, warn=False + ) + cash_score, _ = evaluate_policy( + _TradeOrWaitPolicy(False), model.eval_env, n_eval_episodes=1, warn=False + ) + self.assertAlmostEqual(trade_score, 7.5) + self.assertEqual(cash_score, 0.0) + model.train_env.reset() + _, rewards, _, _ = model.train_env.step([Actions.Long_enter.value]) + np.testing.assert_allclose(rewards, [0.8 * 100 * ratio], rtol=1e-6) + model.close_envs() + + def test_evaluation_retains_entry_and_terminal_exit_additives(self): + with tempfile.TemporaryDirectory() as temp: + fee = 0.001 + config = model_config( + temp, + freqai={ + "rl_config": { + "add_state_info": True, + "model_reward_parameters": { + "hold_potential_enabled": True, + "exit_potential_mode": "non_canonical", + "entry_additive_enabled": True, + "exit_additive_enabled": True, + "max_trade_duration_candles": 1, + }, + } + }, + ) + model = ReforceXY(config=config) + model.live = True + self.addCleanup(model.close_envs) + prices = pd.DataFrame({"open": [100.0] * 3}) + model.set_train_and_eval_environments( + {"train_features": prices.copy(), "test_features": prices.copy()}, + prices, + prices, + SimpleNamespace(pair=PAIR), + ) + model.eval_env.set_attr("fee", fee) + score, _ = evaluate_policy( + _TradeOrWaitPolicy(True), model.eval_env, n_eval_episodes=1, warn=False + ) + pnl = 1 / (1 + fee) ** 2 - 1 + entry_additive = 6.25 * 0.5 * math.tanh(pnl / 0.06) + terminal_additive = 6.25 * 0.5 * (math.tanh(pnl / 0.06) - math.tanh(1.0)) + self.assertAlmostEqual(score, 100 * pnl + entry_additive + terminal_additive, places=6) + + def test_monitored_scores_retain_small_returns_between_large_exits_after_reset(self): + for multiprocessing in (False, True): + with ( + self.subTest(multiprocessing=multiprocessing), + tempfile.TemporaryDirectory() as temp, + ): + base = 2**25 + small_mark = 150.0 * (1.0 + 1.0 / base) + config = model_config( + temp, + freqai={ + "rl_config": { + "n_eval_envs": 2 if multiprocessing else 1, + "eval_multiprocessing": multiprocessing, + "model_reward_parameters": { + "base_factor": base, + "win_reward_factor": 0.0, + "efficiency_weight": 0.0, + "idle_penalty_ratio": 0.0, + }, + } + }, + ) + model = ReforceXY(config=config) + model.live = True + self.addCleanup(model.close_envs) + prices = pd.DataFrame( + { + "open": [ + 100.0, + 100.0, + 150.0, + 150.0, + small_mark, + small_mark, + 1.5 * small_mark, + ] + } + ) + model.set_train_and_eval_environments( + {"train_features": prices.copy(), "test_features": prices.copy()}, + prices, + prices, + SimpleNamespace(pair=PAIR), + ) + model.eval_env.set_attr("fee", 0.0) + for _ in range(2): + model.eval_env.reset() + for action in ( + Actions.Long_enter, + Actions.Long_exit, + Actions.Long_enter, + Actions.Long_exit, + Actions.Short_enter, + ): + _, _, done, infos = model.eval_env.step( + [action.value] * model.eval_env.num_envs + ) + self.assertTrue(done.all()) + # Three exit rewards are 2**24, 1 and -2**24; equity remains positive. + for info in infos: + self.assertEqual(info["episode"]["r"], 1.0) + self.assertGreater(info["total_profit"], 0.0) + model.close_envs() diff --git a/ReforceXY/tests/test_model_training_observations.py b/ReforceXY/tests/test_model_training_observations.py index 0b6221f..751e872 100644 --- a/ReforceXY/tests/test_model_training_observations.py +++ b/ReforceXY/tests/test_model_training_observations.py @@ -97,23 +97,27 @@ class TrainingObservationsTest(QaTestCase): metadata_path = dk.data_path / f"{dk.model_filename}_{METADATA}.json" metadata = json.loads(metadata_path.read_text()) current_generation = metadata[model._DEPLOYMENT_COORDINATE_MARKER_KEY] - metadata[model._DEPLOYMENT_COORDINATE_MARKER_KEY] = ( - "chronological-frozen-pipelines-v2" - ) - metadata_path.write_text(json.dumps(metadata)) - for cached in (False, True): - with self.subTest(legacy_cached=cached): - model.dd.model_dictionary.clear() - model.dd.meta_data_dictionary.clear() - if cached: - inference = model.dd.load_data(dk.pair, dk) - action, _ = inference.predict( - np.zeros(inference.observation_space.shape, dtype=np.float32), - deterministic=True, - ) - self.assertTrue(inference.action_space.contains(action)) - with self.assertRaisesRegex(DependencyException, "Reset trained models"): - model._resolve_deployment_state(dk, dk.pair) + for legacy_generation in ( + "chronological-frozen-pipelines-v2", + "chronological-frozen-pipelines-pbrs-v3", + ): + metadata[model._DEPLOYMENT_COORDINATE_MARKER_KEY] = legacy_generation + metadata_path.write_text(json.dumps(metadata)) + for cached in (False, True): + with self.subTest( + legacy_generation=legacy_generation, legacy_cached=cached + ): + model.dd.model_dictionary.clear() + model.dd.meta_data_dictionary.clear() + if cached: + inference = model.dd.load_data(dk.pair, dk) + action, _ = inference.predict( + np.zeros(inference.observation_space.shape, dtype=np.float32), + deterministic=True, + ) + self.assertTrue(inference.action_space.contains(action)) + with self.assertRaises(DependencyException): + model._resolve_deployment_state(dk, dk.pair) metadata[model._DEPLOYMENT_COORDINATE_MARKER_KEY] = current_generation metadata_path.write_text(json.dumps(metadata)) model.dd.meta_data_dictionary.clear() diff --git a/ReforceXY/user_data/freqaimodels/ReforceXY.py b/ReforceXY/user_data/freqaimodels/ReforceXY.py index 6cb2410..5598ba6 100644 --- a/ReforceXY/user_data/freqaimodels/ReforceXY.py +++ b/ReforceXY/user_data/freqaimodels/ReforceXY.py @@ -85,9 +85,45 @@ from stable_baselines3.common.vec_env import ( DummyVecEnv, SubprocVecEnv, VecEnv, + VecEnvWrapper, VecFrameStack, VecMonitor, ) +from stable_baselines3.common.vec_env.base_vec_env import VecEnvObs, VecEnvStepReturn + + +class _SelectionRewardVecEnv(VecEnvWrapper): + """Expose non-PBRS evaluation rewards before VecMonitor aggregates episodes. + + The repository does not use VecNormalize. Adding it requires aligning train + and evaluation wrapper chains for SB3's normalization synchronization. + """ + + def reset(self) -> VecEnvObs: + return self.venv.reset() + + def step_wait(self) -> VecEnvStepReturn: + observations, _, dones, infos = self.venv.step_wait() + rewards = np.fromiter( + (float(info["reward_selection"]) for info in infos), + dtype=np.float64, + count=self.num_envs, + ) + return observations, rewards, dones, infos + + +class _RewardVecMonitor(VecMonitor): + """Accumulate native episode scores without narrowing to float32.""" + + def __init__(self, venv: VecEnv): + super().__init__(venv) + self.episode_returns = np.zeros(self.num_envs, dtype=np.float64) + + def reset(self) -> VecEnvObs: + observations = super().reset() + # VecMonitor.reset recreates float32 storage; keep native reward precision. + self.episode_returns = np.zeros(self.num_envs, dtype=np.float64) + return observations def _update_eval_best_reward(callback: Any, mean_reward: float, model: Any) -> None: @@ -437,7 +473,7 @@ class ReforceXY(BaseReinforcementLearningModel): _LOG_2: Final[float] = math.log(2.0) _DEPLOYMENT_COORDINATE_MARKER_KEY: Final[str] = "reforcexy_deployment_coordinates" - _DEPLOYMENT_COORDINATE_GENERATION: Final[str] = "chronological-frozen-pipelines-pbrs-v3" + _DEPLOYMENT_COORDINATE_GENERATION: Final[str] = "chronological-frozen-pipelines-pbrs-v4" DEFAULT_BASE_FACTOR: Final[float] = 100.0 DEFAULT_DISCOUNT_GAMMA: Final[float] = 0.95 @@ -539,12 +575,14 @@ class ReforceXY(BaseReinforcementLearningModel): *_OPTIMIZER_CLASSES_OPTUNA, "adam", ) - _NET_ARCH_SIZES: Final[tuple[NetArchSize, ...]] = ( - "small", - "medium", - "large", - "extra_large", - ) + # Shared by named architectures and plain-configuration fallbacks. + _NET_ARCH_WIDTHS: Final[dict[NetArchSize, int]] = { + "small": 128, + "medium": 256, + "large": 512, + "extra_large": 1024, + } + _NET_ARCH_SIZES: Final[tuple[NetArchSize, ...]] = tuple(_NET_ARCH_WIDTHS) _STORAGE_BACKENDS: Final[tuple[StorageBackend, ...]] = ("sqlite", "file") _SAMPLERS: Final[_Samplers] = _Samplers() _JOURNAL_TAIL_PROBE_BYTES: Final[int] = 64 * 1024 @@ -561,9 +599,7 @@ class ReforceXY(BaseReinforcementLearningModel): _QUARANTINE_TIE_BREAK_LIMIT: Final[int] = 99 _BEST_PARAMS_LOCK_FILENAME: Final[str] = ".hyperopt-best-params.lock" # Bump on objective changes that make persisted trials or best params incompatible. - _OPTUNA_OBJECTIVE_IDENTITY: Final[str] = ( - "terminal-liquidation-risk-normalized-trained-policy-v5" - ) + _OPTUNA_OBJECTIVE_IDENTITY: Final[str] = "unshaped-evaluation-temporal-costs-trained-policy-v6" _PPO_N_STEPS: Final[tuple[int, ...]] = (512, 1024, 2048, 4096) _PPO_N_STEPS_MIN: Final[int] = min(_PPO_N_STEPS) _PPO_N_STEPS_MAX: Final[int] = max(_PPO_N_STEPS) @@ -1143,7 +1179,8 @@ class ReforceXY(BaseReinforcementLearningModel): if not model_params.get("policy_kwargs"): model_params["policy_kwargs"] = {} - default_net_arch: list[int] = [128, 128] + default_width = ReforceXY._NET_ARCH_WIDTHS[ReforceXY._NET_ARCH_SIZES[0]] + default_net_arch: list[int] = [default_width, default_width] net_arch: list[int] | dict[str, list[int]] | NetArchSize = model_params.get( "policy_kwargs", {} ).get("net_arch", default_net_arch) @@ -2850,9 +2887,9 @@ class ReforceXY(BaseReinforcementLearningModel): if eval_env is not None: eval_env = VecFrameStack(eval_env, n_stack=self.frame_stacking) - train_env = VecMonitor(train_env) + train_env = _RewardVecMonitor(train_env) if eval_env is not None: - eval_env = VecMonitor(eval_env) + eval_env = _RewardVecMonitor(_SelectionRewardVecEnv(eval_env)) return train_env, eval_env except BaseException: @@ -3160,11 +3197,11 @@ class MyRLEnv(Base5ActionRLEnv): ReforceXY.DEFAULT_MAX_TRADE_DURATION_CANDLES, ) ) + max_idle_duration = model_reward_parameters.get("max_idle_duration_candles") self.max_idle_duration_candles: int = int( - model_reward_parameters.get( - "max_idle_duration_candles", - ReforceXY.DEFAULT_IDLE_DURATION_MULTIPLIER * self.max_trade_duration_candles, - ) + ReforceXY.DEFAULT_IDLE_DURATION_MULTIPLIER * self.max_trade_duration_candles + if max_idle_duration is None + else max_idle_duration ) # === PBRS COMMON PARAMETERS === self._potential_gamma = ReforceXY._resolve_discount_gamma( @@ -3796,12 +3833,12 @@ class MyRLEnv(Base5ActionRLEnv): def _power(dr: float, p: Mapping[str, Any]) -> float: tau = p.get("exit_power_tau") - if isinstance(tau, (int, float)): + if isinstance(tau, (int, float, np.integer, np.floating)): tau = float(tau) alpha = -math.log(tau) / ReforceXY._LOG_2 if 0.0 < tau <= 1.0 else 1.0 else: alpha = 1.0 - return 1.0 / math.pow(1.0 + dr, alpha) + return math.pow(1.0 + dr, -alpha) def _half_life(dr: float, p: Mapping[str, Any]) -> float: hl = float(p.get("exit_half_life", ReforceXY.DEFAULT_EXIT_HALF_LIFE)) @@ -3925,7 +3962,8 @@ class MyRLEnv(Base5ActionRLEnv): self, pnl: float, pnl_target: float, model_reward_parameters: Mapping[str, Any] ) -> float: """ - Compute PnL target coefficient (typically 0.5-2.0) using tanh on PnL/target ratio. + Compute PnL amplification: [1, 1 + win_reward_factor] for gains, + and [1, 1 + win_reward_factor * rr] for losses with nonnegative tunables. """ pnl_target_coefficient = 1.0 @@ -4019,10 +4057,9 @@ class MyRLEnv(Base5ActionRLEnv): # 1. Invalid action if not self.action_masking and not self._is_valid(action): self.tensorboard_log("invalid", category="actions") - base_reward = float( + self._last_invalid_penalty = float( model_reward_parameters.get("invalid_action", ReforceXY.DEFAULT_INVALID_ACTION) ) - self._last_invalid_penalty = float(base_reward) max_trade_duration = max(1, self.max_trade_duration_candles) trade_duration = self.get_trade_duration() @@ -4032,13 +4069,10 @@ class MyRLEnv(Base5ActionRLEnv): ) idle_factor = base_factor * (self.profit_aim / self.rr) hold_factor = idle_factor + unchanged_position = not self.is_tradesignal(action) # 2. Idle penalty - if ( - base_reward is None - and action == Actions.Neutral.value - and self._position == Positions.Neutral - ): + if base_reward is None and unchanged_position and self._position == Positions.Neutral: max_idle_duration = max(1, self.max_idle_duration_candles) idle_penalty_ratio = float( model_reward_parameters.get( @@ -4061,7 +4095,7 @@ class MyRLEnv(Base5ActionRLEnv): if ( base_reward is None and self._position in (Positions.Short, Positions.Long) - and action == Actions.Neutral.value + and unchanged_position ): hold_penalty_ratio = float( model_reward_parameters.get( @@ -4106,7 +4140,7 @@ class MyRLEnv(Base5ActionRLEnv): if base_reward is None: base_reward = 0.0 - return base_reward + return base_reward + self._last_invalid_penalty def _get_observation(self) -> NDArray[np.float32]: start_idx = max(0, self._current_tick - self.window_size) @@ -4212,6 +4246,7 @@ class MyRLEnv(Base5ActionRLEnv): execution_tick = self._current_tick exit_pnl = None reward = self.calculate_reward(action) + selection_reward = reward trade_type = self.execute_trade(action) entry_pnl = self.get_unrealized_profit() if previous_position == Positions.Neutral else 0.0 if trade_type is not None and self._position in (Positions.Long, Positions.Short): @@ -4258,6 +4293,7 @@ class MyRLEnv(Base5ActionRLEnv): exit_additive_scale=self._exit_additive_ratio * base_factor, ) reward += shaping + entry_additive + exit_additive + selection_reward += entry_additive + exit_additive terminated = self.is_terminated() terminal_liquidation = terminated and self._position in (Positions.Long, Positions.Short) if terminal_liquidation: @@ -4274,6 +4310,7 @@ class MyRLEnv(Base5ActionRLEnv): self.rl_config.get("model_reward_parameters", {}), ) reward += liquidation_reward + selection_reward += liquidation_reward self._last_exit_reward += liquidation_reward if self._exit_additive_enabled and not self.is_pbrs_invariant_mode(): liquidation_additive = self._compute_exit_additive( @@ -4283,6 +4320,7 @@ class MyRLEnv(Base5ActionRLEnv): self._exit_additive_ratio * base_factor, ) reward += liquidation_additive + selection_reward += liquidation_additive self._last_exit_additive += liquidation_additive self._total_exit_additive += liquidation_additive exit_pnl = terminal_pnl @@ -4333,6 +4371,7 @@ class MyRLEnv(Base5ActionRLEnv): "reward_hold": round(self._last_hold_penalty, 5), "reward_exit": round(self._last_exit_reward, 5), "reward": round(reward, 5), + "reward_selection": selection_reward, "total_reward": round(self.total_reward, 5), "pbrs_invariant": self.is_pbrs_invariant_mode(), "idle_duration": idle_duration, @@ -5389,34 +5428,13 @@ def get_schedule( def get_net_arch(model_type: str, net_arch_type: NetArchSize) -> list[int] | dict[str, list[int]]: - """ - Get network architecture - """ + """Build only the selected architecture, falling back to the smallest size.""" + width = ReforceXY._NET_ARCH_WIDTHS.get( + net_arch_type, ReforceXY._NET_ARCH_WIDTHS[ReforceXY._NET_ARCH_SIZES[0]] + ) if ReforceXY._MODEL_TYPES[0] in model_type: # "PPO" - return { - ReforceXY._NET_ARCH_SIZES[0]: { - "pi": [128, 128], - "vf": [128, 128], - }, # "small" - ReforceXY._NET_ARCH_SIZES[1]: { - "pi": [256, 256], - "vf": [256, 256], - }, # "medium" - ReforceXY._NET_ARCH_SIZES[2]: { - "pi": [512, 512], - "vf": [512, 512], - }, # "large" - ReforceXY._NET_ARCH_SIZES[3]: { - "pi": [1024, 1024], - "vf": [1024, 1024], - }, # "extra_large" - }.get(net_arch_type, {"pi": [128, 128], "vf": [128, 128]}) - return { - ReforceXY._NET_ARCH_SIZES[0]: [128, 128], # "small" - ReforceXY._NET_ARCH_SIZES[1]: [256, 256], # "medium" - ReforceXY._NET_ARCH_SIZES[2]: [512, 512], # "large" - ReforceXY._NET_ARCH_SIZES[3]: [1024, 1024], # "extra_large" - }.get(net_arch_type, [128, 128]) + return {"pi": [width, width], "vf": [width, width]} + return [width, width] def get_activation_fn(