]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
fix: preserve prediction validity and harmonize strategy safeguards (#291)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Mon, 19 Oct 2026 20:05:30 +0000 (22:05 +0200)
committerGitHub <noreply@github.com>
Mon, 19 Oct 2026 20:05:30 +0000 (22:05 +0200)
Preserve QuickAdapter prediction availability, causal replay and model metrics. Bind backtest caches to effective inputs, code, configuration and resolved native parameters.

Harmonize RL production provenance, historical schema restoration, numerical safeguards and QA contracts. Update operator documentation and diagnostic snapshots.

27 files changed:
CONTRIBUTING.md
ReforceXY/.basedpyright/diagnostics.json
ReforceXY/README.md
ReforceXY/tests/qa_support.py
ReforceXY/tests/test_coverage_floor.py
ReforceXY/tests/test_model_review_contracts.py
ReforceXY/tests/test_model_training_observations.py
ReforceXY/tests/test_strategy_semantics.py
ReforceXY/tests/test_suite_contract.py
ReforceXY/user_data/freqaimodels/ReforceXY.py
ReforceXY/user_data/strategies/RLAgentStrategy.py
docs/evaluation.md
quickadapter/.basedpyright/diagnostics.json
quickadapter/README.md
quickadapter/tests/qa_support.py
quickadapter/tests/test_coverage_floor.py
quickadapter/tests/test_regressor_data_split.py
quickadapter/tests/test_regressor_optuna_study.py
quickadapter/tests/test_regressor_pipeline_contracts.py [new file with mode: 0644]
quickadapter/tests/test_regressor_prediction_dedup.py
quickadapter/tests/test_regressor_prediction_history.py
quickadapter/tests/test_strategy_pure.py
quickadapter/tests/test_strategy_sizing.py
quickadapter/tests/test_suite_contract.py
quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py
quickadapter/user_data/strategies/QuickAdapterV3.py
quickadapter/user_data/strategies/Utils.py

index 92d2eaef131c0023ac233bf52c11b392910aeaab..11545bac99f44f14f0dd189bb5f6a4561bd1c993 100644 (file)
@@ -99,10 +99,20 @@ disabled branch trace, a disabled namespace walk, a source tree that is not the
 measured one, a `fail_under` that has drifted into the inert `[run]` section, the
 source-level `no cover` or `no branch` pragmas (with or without a colon),
 ellipsis-only bodies, additional `TYPE_CHECKING` blocks beyond the existing
-import-only block, and any `omit`, `include`, `exclude_lines`, `exclude_also` or
-`partial_*` of production code. The shared `scripts/run-coverage.sh` runner is
-exercised with real passing and failing coverage reports and a failing test suite;
-the checks assert process exit status, not workflow token spelling.
+import-only block, and any `omit`, `include`, `patch`, `exclude_lines`,
+`exclude_also` or `partial_*` of production code. Both guards validate legal options
+and exact sections, reject extra measurement roots and plugins, and compare
+coverage's actual statement and branch denominators with default exclusions enabled
+and disabled. QuickAdapter permits only its existing import-only `TYPE_CHECKING`
+block; its runtime fallback remains measured. The shared `scripts/run-coverage.sh`
+runner is exercised with real passing and failing coverage reports and a failing
+test suite; checks assert process exit status, not workflow token spelling.
+
+Import guards require one registration per resolved production file. QuickAdapter
+strategy modules use bare names and its model uses a dotted name; ReforceXY uses
+dotted production names. Test modules use bare stems in both suites. QA config
+builders preserve explicit run modes, user directories and nested overrides;
+fixtures exercising futures behavior must request futures explicitly.
 
 ## Quality checks
 
index 5db74eeb27cffc52857aa91d1581129f9ce25b7d..530d90be1258caa9a2f344d7aa7456979a3ad0cf 100644 (file)
     },
     {
       "endCharacter": 22,
-      "endLine": 1025,
+      "endLine": 1059,
       "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": 1025
+      "startLine": 1059
     },
     {
       "endCharacter": 37,
-      "endLine": 1025,
+      "endLine": 1059,
       "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": 1025
+      "startLine": 1059
     },
     {
       "endCharacter": 24,
-      "endLine": 1284,
+      "endLine": 1318,
       "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": 1284
+      "startLine": 1318
     },
     {
       "endCharacter": 38,
-      "endLine": 1517,
+      "endLine": 1551,
       "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": 1517
+      "startLine": 1551
     },
     {
       "endCharacter": 36,
-      "endLine": 1521,
+      "endLine": 1555,
       "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": 1521
+      "startLine": 1555
     },
     {
       "endCharacter": 80,
-      "endLine": 1524,
+      "endLine": 1558,
       "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": 1524
+      "startLine": 1558
     },
     {
       "endCharacter": 78,
-      "endLine": 1525,
+      "endLine": 1559,
       "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": 1525
+      "startLine": 1559
     },
     {
       "endCharacter": 80,
-      "endLine": 1526,
+      "endLine": 1560,
       "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": 1526
+      "startLine": 1560
     },
     {
       "endCharacter": 56,
-      "endLine": 1610,
+      "endLine": 1644,
       "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": 1610
+      "startLine": 1644
     },
     {
       "endCharacter": 84,
-      "endLine": 1630,
+      "endLine": 1664,
       "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": 1630
+      "startLine": 1664
     },
     {
       "endCharacter": 84,
-      "endLine": 1630,
+      "endLine": 1664,
       "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": 1630
+      "startLine": 1664
     },
     {
       "endCharacter": 44,
-      "endLine": 2704,
+      "endLine": 2742,
       "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py",
       "message": "\"best_trial_params\" is possibly unbound",
       "rule": "reportPossiblyUnboundVariable",
       "severity": "error",
       "startCharacter": 27,
-      "startLine": 2704
+      "startLine": 2742
     },
     {
       "endCharacter": 12,
-      "endLine": 2724,
+      "endLine": 2762,
       "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": 2724
+      "startLine": 2762
     },
     {
       "endCharacter": 16,
-      "endLine": 2725,
+      "endLine": 2763,
       "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": 2725
+      "startLine": 2763
     },
     {
       "endCharacter": 51,
-      "endLine": 2768,
+      "endLine": 2806,
       "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": 2768
+      "startLine": 2806
     },
     {
       "endCharacter": 53,
-      "endLine": 2774,
+      "endLine": 2812,
       "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": 2774
+      "startLine": 2812
     },
     {
       "endCharacter": 22,
-      "endLine": 2837,
+      "endLine": 2875,
       "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py",
       "message": "Operator \"*\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 15,
-      "startLine": 2837
+      "startLine": 2875
     },
     {
       "endCharacter": 96,
-      "endLine": 2839,
+      "endLine": 2877,
       "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py",
       "message": "Operator \"*\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 89,
-      "startLine": 2839
+      "startLine": 2877
     },
     {
       "endCharacter": 23,
-      "endLine": 2842,
+      "endLine": 2880,
       "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py",
       "message": "Operator \"*\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 2842
+      "startLine": 2880
     },
     {
       "endCharacter": 98,
-      "endLine": 2844,
+      "endLine": 2882,
       "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py",
       "message": "Operator \"*\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 91,
-      "startLine": 2844
+      "startLine": 2882
     },
     {
       "endCharacter": 44,
-      "endLine": 2856,
+      "endLine": 2894,
       "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": 2856
+      "startLine": 2894
     },
     {
       "endCharacter": 133,
-      "endLine": 2858,
+      "endLine": 2896,
       "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": 2858
+      "startLine": 2896
     },
     {
       "endCharacter": 30,
-      "endLine": 2861,
+      "endLine": 2899,
       "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py",
       "message": "Operator \">\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 15,
-      "startLine": 2861
+      "startLine": 2899
     },
     {
       "endCharacter": 54,
-      "endLine": 2927,
+      "endLine": 2965,
       "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py",
       "message": "\"is_pruned\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 45,
-      "startLine": 2927
+      "startLine": 2965
     },
     {
       "endCharacter": 67,
-      "endLine": 2930,
+      "endLine": 2968,
       "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py",
       "message": "\"use_masking\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 56,
-      "startLine": 2930
+      "startLine": 2968
     },
     {
       "endCharacter": 60,
-      "endLine": 2938,
+      "endLine": 2976,
       "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": 2938
+      "startLine": 2976
     },
     {
       "endCharacter": 84,
-      "endLine": 2938,
+      "endLine": 2976,
       "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": 2938
+      "startLine": 2976
     },
     {
       "endCharacter": 84,
-      "endLine": 2938,
+      "endLine": 2976,
       "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": 2938
+      "startLine": 2976
     },
     {
       "endCharacter": 46,
-      "endLine": 2992,
+      "endLine": 3030,
       "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py",
       "message": "\"is_pruned\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 37,
-      "startLine": 2992
+      "startLine": 3030
     },
     {
       "endCharacter": 57,
-      "endLine": 2995,
+      "endLine": 3033,
       "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": 2995
+      "startLine": 3033
     },
     {
       "endCharacter": 37,
-      "endLine": 3005,
+      "endLine": 3043,
       "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": 3005
+      "startLine": 3043
     },
     {
       "endCharacter": 36,
-      "endLine": 3010,
+      "endLine": 3048,
       "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": 3010
+      "startLine": 3048
     },
     {
       "endCharacter": 7,
-      "endLine": 3055,
+      "endLine": 3093,
       "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": 3055
+      "startLine": 3093
     },
     {
       "endCharacter": 36,
-      "endLine": 3067,
+      "endLine": 3105,
       "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": 3067
+      "startLine": 3105
     },
     {
       "endCharacter": 13,
-      "endLine": 3659,
+      "endLine": 3697,
       "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": 3659
+      "startLine": 3697
     },
     {
       "endCharacter": 26,
-      "endLine": 3680,
+      "endLine": 3718,
       "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": 3680
+      "startLine": 3718
     },
     {
       "endCharacter": 55,
-      "endLine": 3988,
+      "endLine": 4026,
       "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": 3988
+      "startLine": 4026
     },
     {
       "endCharacter": 55,
-      "endLine": 3988,
+      "endLine": 4026,
       "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": 3988
+      "startLine": 4026
     },
     {
       "endCharacter": 59,
-      "endLine": 4012,
+      "endLine": 4050,
       "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": 4012
+      "startLine": 4050
     },
     {
       "endCharacter": 59,
-      "endLine": 4012,
+      "endLine": 4050,
       "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": 4012
+      "startLine": 4050
     },
     {
       "endCharacter": 24,
-      "endLine": 4041,
+      "endLine": 4079,
       "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": 4041
+      "startLine": 4079
     },
     {
       "endCharacter": 12,
-      "endLine": 4134,
+      "endLine": 4172,
       "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": 4134
+      "startLine": 4172
     },
     {
       "endCharacter": 20,
-      "endLine": 4324,
+      "endLine": 4362,
       "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": 4324
+      "startLine": 4362
     },
     {
       "endCharacter": 40,
-      "endLine": 4473,
+      "endLine": 4511,
       "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py",
       "message": "\"Figure\" is not exported from module \"matplotlib.pyplot\"",
       "rule": "reportPrivateImportUsage",
       "severity": "error",
       "startCharacter": 34,
-      "startLine": 4473
+      "startLine": 4511
     },
     {
       "endCharacter": 54,
-      "endLine": 4976,
+      "endLine": 5014,
       "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": 4976
+      "startLine": 5014
     },
     {
       "endCharacter": 54,
-      "endLine": 4976,
+      "endLine": 5014,
       "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": 4976
+      "startLine": 5014
     },
     {
       "endCharacter": 50,
-      "endLine": 4988,
+      "endLine": 5026,
       "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": 4988
+      "startLine": 5026
     },
     {
       "endCharacter": 72,
-      "endLine": 4999,
+      "endLine": 5037,
       "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": 4999
+      "startLine": 5037
     },
     {
       "endCharacter": 72,
-      "endLine": 4999,
+      "endLine": 5037,
       "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": 4999
+      "startLine": 5037
     },
     {
       "endCharacter": 73,
-      "endLine": 5008,
+      "endLine": 5046,
       "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": 5008
+      "startLine": 5046
     },
     {
       "endCharacter": 73,
-      "endLine": 5008,
+      "endLine": 5046,
       "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": 5008
+      "startLine": 5046
     },
     {
       "endCharacter": 58,
-      "endLine": 5017,
+      "endLine": 5055,
       "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": 5017
+      "startLine": 5055
     },
     {
       "endCharacter": 58,
-      "endLine": 5017,
+      "endLine": 5055,
       "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": 5017
+      "startLine": 5055
     },
     {
       "endCharacter": 50,
-      "endLine": 5248,
+      "endLine": 5286,
       "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": 5248
+      "startLine": 5286
     },
     {
       "endCharacter": 20,
-      "endLine": 5425,
+      "endLine": 5463,
       "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": 5425
+      "startLine": 5463
     },
     {
       "endCharacter": 20,
-      "endLine": 5425,
+      "endLine": 5463,
       "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": 5425
+      "startLine": 5463
     },
     {
       "endCharacter": 59,
-      "endLine": 5430,
+      "endLine": 5468,
       "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": 5430
+      "startLine": 5468
     },
     {
       "endCharacter": 59,
-      "endLine": 5430,
+      "endLine": 5468,
       "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": 5430
+      "startLine": 5468
     },
     {
       "endCharacter": 65,
-      "endLine": 5431,
+      "endLine": 5469,
       "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": 5431
+      "startLine": 5469
     },
     {
       "endCharacter": 65,
-      "endLine": 5431,
+      "endLine": 5469,
       "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": 5431
+      "startLine": 5469
     },
     {
       "endCharacter": 63,
-      "endLine": 5434,
+      "endLine": 5472,
       "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": 5434
+      "startLine": 5472
     },
     {
       "endCharacter": 63,
-      "endLine": 5434,
+      "endLine": 5472,
       "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": 5434
+      "startLine": 5472
     },
     {
       "endCharacter": 61,
-      "endLine": 5436,
+      "endLine": 5474,
       "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": 5436
+      "startLine": 5474
     },
     {
       "endCharacter": 61,
-      "endLine": 5436,
+      "endLine": 5474,
       "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": 5436
+      "startLine": 5474
     },
     {
       "endCharacter": 67,
-      "endLine": 5437,
+      "endLine": 5475,
       "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": 5437
+      "startLine": 5475
     },
     {
       "endCharacter": 67,
-      "endLine": 5437,
+      "endLine": 5475,
       "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": 5437
+      "startLine": 5475
     },
     {
       "endCharacter": 73,
-      "endLine": 5438,
+      "endLine": 5476,
       "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": 5438
+      "startLine": 5476
     },
     {
       "endCharacter": 73,
-      "endLine": 5438,
+      "endLine": 5476,
       "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": 5438
+      "startLine": 5476
     },
     {
       "endCharacter": 61,
-      "endLine": 5439,
+      "endLine": 5477,
       "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": 5439
+      "startLine": 5477
     },
     {
       "endCharacter": 61,
-      "endLine": 5439,
+      "endLine": 5477,
       "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": 5439
+      "startLine": 5477
     },
     {
       "endCharacter": 89,
-      "endLine": 5446,
+      "endLine": 5484,
       "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": 5446
+      "startLine": 5484
     },
     {
       "endCharacter": 89,
-      "endLine": 5446,
+      "endLine": 5484,
       "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": 5446
+      "startLine": 5484
     },
     {
       "endCharacter": 83,
-      "endLine": 5447,
+      "endLine": 5485,
       "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": 5447
+      "startLine": 5485
     },
     {
       "endCharacter": 83,
-      "endLine": 5447,
+      "endLine": 5485,
       "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": 5447
+      "startLine": 5485
     },
     {
       "endCharacter": 65,
-      "endLine": 5473,
+      "endLine": 5511,
       "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": 5473
+      "startLine": 5511
     },
     {
       "endCharacter": 65,
-      "endLine": 5473,
+      "endLine": 5511,
       "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": 5473
+      "startLine": 5511
     },
     {
       "endCharacter": 67,
-      "endLine": 5475,
+      "endLine": 5513,
       "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": 5475
+      "startLine": 5513
     },
     {
       "endCharacter": 67,
-      "endLine": 5475,
+      "endLine": 5513,
       "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": 5475
+      "startLine": 5513
     },
     {
       "endCharacter": 87,
-      "endLine": 5478,
+      "endLine": 5516,
       "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": 5478
+      "startLine": 5516
     },
     {
       "endCharacter": 87,
-      "endLine": 5478,
+      "endLine": 5516,
       "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": 5478
+      "startLine": 5516
     },
     {
       "endCharacter": 93,
-      "endLine": 5479,
+      "endLine": 5517,
       "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": 5479
+      "startLine": 5517
     },
     {
       "endCharacter": 93,
-      "endLine": 5479,
+      "endLine": 5517,
       "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": 5479
+      "startLine": 5517
     },
     {
       "endCharacter": 89,
-      "endLine": 5480,
+      "endLine": 5518,
       "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": 5480
+      "startLine": 5518
     },
     {
       "endCharacter": 89,
-      "endLine": 5480,
+      "endLine": 5518,
       "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": 5480
+      "startLine": 5518
     },
     {
       "endCharacter": 89,
-      "endLine": 5481,
+      "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": 46,
-      "startLine": 5481
+      "startLine": 5519
     },
     {
       "endCharacter": 89,
-      "endLine": 5481,
+      "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": 46,
-      "startLine": 5481
+      "startLine": 5519
     },
     {
       "endCharacter": 75,
-      "endLine": 5482,
+      "endLine": 5520,
       "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": 5482
+      "startLine": 5520
     },
     {
       "endCharacter": 75,
-      "endLine": 5482,
+      "endLine": 5520,
       "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": 5482
+      "startLine": 5520
     },
     {
       "endCharacter": 33,
       "severity": "error",
       "startCharacter": 8,
       "startLine": 67
-    },
-    {
-      "endCharacter": 72,
-      "endLine": 89,
-      "file": "ReforceXY/user_data/strategies/RLAgentStrategy.py",
-      "message": "No overloads for \"__call__\" match the provided arguments",
-      "rule": "reportCallIssue",
-      "severity": "error",
-      "startCharacter": 42,
-      "startLine": 89
-    },
-    {
-      "endCharacter": 71,
-      "endLine": 89,
-      "file": "ReforceXY/user_data/strategies/RLAgentStrategy.py",
-      "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"x1\" of type \"_SupportsArrayUFunc\" in function \"__call__\"\n  Type \"Series[Any] | None\" is not assignable to type \"_SupportsArrayUFunc\"\n    \"None\" is incompatible with protocol \"_SupportsArrayUFunc\"\n      \"__array_ufunc__\" is not present",
-      "rule": "reportArgumentType",
-      "severity": "error",
-      "startCharacter": 49,
-      "startLine": 89
     }
   ],
   "filesAnalyzed": 3,
index 3ff8d9eabb6141d833c2ef9400d88d2fdc64ee9f..04208f4a076729ca61feb8b0ba78ac3ae334effd 100644 (file)
@@ -91,6 +91,11 @@ volume, calendar and raw OHLC features, but does not implement a period-dependen
 `freqai.feature_parameters.indicator_periods_candles=[8,16,32]` therefore adds
 no period-dependent indicators.
 
+Close log returns require finite, strictly positive closes. An invalid close and
+the following candle have NaN returns; returns resume after two consecutive valid
+closes. Prices, rows and volume remain unchanged. Native feature filtering removes
+affected training rows and invalidates affected prediction windows.
+
 Explicit configuration overrides schema defaults and constructor fallbacks. The
 tables distinguish those defaults and requirements from the supplied profile;
 inspect normalized startup logs. Set `test_size`
@@ -253,6 +258,11 @@ including rows in partly marked histories, can count when their nonzero,
 nonexpired prediction status distinguishes them from bootstrap; zero-status rows
 remain excluded because bootstrap and rejected predictions cannot be
 distinguished. Explicit false markers remain excluded.
+Real policy outputs count even when their prediction status is rejected, including
+genuine neutral actions. Neutral placeholders without policy inference, expired
+outputs and skipped candles do not count. New metadata fields added to restored
+history remain NaN for earlier candles; current metadata never backfills the past.
+These unknown historical values survive restart initialization.
 On duplicate candle dates, a provable prediction takes precedence over an
 ambiguous close-bearing legacy row during history restoration.
 Rows with an invalid `date_pred` are discarded with a per-pair warning and the
index 4ebedf33e1efcf18d4dadc08038b8fabb390eca0..3ff67250ebcbe109f45dac70c689ceecdfce8b9a 100644 (file)
@@ -89,10 +89,8 @@ def _merge(base: dict[str, Any], overrides: dict[str, Any]) -> dict[str, Any]:
 def model_config(tmp_path: Path | str, **overrides: Any) -> dict[str, Any]:
     """Return a private Freqtrade config rooted at `tmp_path`, deep-merged with `overrides`.
 
-    `runmode` is applied only when the caller supplied none, so a test can ask for
-    RunMode.BACKTEST and get it. QuickAdapter assigns it after the merge and is therefore
-    not overridable; copying that order here would silently hand DRY_RUN to the backtest
-    regressions.
+    Explicit run mode and user directory overrides are preserved. The selected directory
+    is normalized to Path; DRY_RUN applies only when the caller omitted runmode.
     """
     config = _merge(_BASE_CONFIG, overrides)
     config["user_data_dir"] = Path(config.get("user_data_dir", tmp_path))
index 6e3ab61d79dd2e197f04b648f3bb34b51890494c..f74f4e37fe03f2d6efe8864f653008053fb3e682 100644 (file)
@@ -167,17 +167,17 @@ class CoverageFloorTest(QaTestCase):
         # policy allows.
         self.assertGreaterEqual(float(self._raw_floor()), MINIMUM_FLOOR)
 
-    def test_no_production_source_widens_coverage_s_default_exclusions(self):
+    def _assert_source_exclusions(self, tree: Path):
         # The SOURCE side of the same lever. coverage applies DEFAULT_EXCLUDE by
         # regex-searching the raw file text, so a pragma inside a string literal removes a
         # real statement, and `while True:` / `if True:` remove branch units — 27% of this
         # denominator — with no config key reachable at all. Parsing twice, once with the
         # defaults and once without, is exact by construction: coverage applies them only
         # when `self.exclude` is truthy, so the difference is the lever and nothing else.
-        measured = {Path(p) for p in find_python_files(str(MEASURED_TREE), True)}
+        measured = {Path(p) for p in find_python_files(str(tree), True)}
         self.assertEqual(
             measured,
-            set(MEASURED_TREE.rglob("*.py")),
+            set(tree.rglob("*.py")),
             "the guard must audit exactly the files coverage measures; find_python_files also "
             "admits .pyw and rejects odd-character names, so a future one turns this red with "
             "no coverage defect",
@@ -208,6 +208,9 @@ class CoverageFloorTest(QaTestCase):
                     "a branch-level default exclusion matches here",
                 )
 
+    def test_no_production_source_widens_coverage_s_default_exclusions(self):
+        self._assert_source_exclusions(MEASURED_TREE)
+
     def test_the_config_is_the_one_coverage_resolves(self):
         # coverage looks for `.coveragerc` in the directory it is run from and does not
         # search parents, so this runs from the strategy directory with the variable
@@ -294,6 +297,33 @@ class CoverageFloorTest(QaTestCase):
 
 
 class CoverageProvenanceRegressionTest(QaTestCase):
+    def test_actual_denominator_guards_reject_config_and_source_exclusions(self):
+        baseline = COVERAGERC.read_text()
+        cases = (
+            ("source_dirs = elsewhere", "test_no_other_option_declares_a_measurement_root"),
+            ("plugins = plugin", "test_no_other_option_declares_a_measurement_root"),
+            ("patch = subprocess", "test_no_production_code_is_excluded_by_any_other_key"),
+            ("exclude_line = return", "test_every_present_option_is_a_legal_name_for_its_section"),
+        )
+        for option, method in cases:
+            with self.subTest(option=option), temporary_directory() as directory:
+                fixture = directory / ".coveragerc"
+                fixture.write_text(baseline.replace("[run]", f"[run]\n{option}"))
+                result = unittest.TestResult()
+                with mock.patch.dict(globals(), COVERAGERC=fixture):
+                    CoverageFloorTest(method).run(result)
+                self.assertEqual(result.errors, [])
+                self.assertEqual(len(result.failures), 1)
+        for source in (
+            "def omitted():\n    ...\n",
+            "def choose(flag):\n    if True:\n        return flag\n    return None\n",
+            'def choose(flag):\n    marker = "# pragma: no cover"\n    return flag\n',
+        ):
+            with self.subTest(source=source), temporary_directory() as directory:
+                (directory / "sample.py").write_text(source)
+                with self.assertRaises(AssertionError):
+                    CoverageFloorTest()._assert_source_exclusions(directory)
+
     def test_actual_guard_rejects_invalid_and_accepts_final_dated_measurements(self):
         """Run the real guard against fixture files, including stale but valid provenance."""
         baseline = COVERAGERC.read_text()
index 8e442c2676189683a03f587c2d29730e4ce75ef8..6048bc6d4022499da2aa508ac199eba0dfcc9544 100644 (file)
@@ -166,6 +166,117 @@ class ReviewContractsTest(QaTestCase):
         model.rl_model_predict(pd.DataFrame({"f": [60.0]}), dk, replacement)
         np.testing.assert_array_equal(replacement.observations[-1], [[[0.0, 60.0]]])
 
+    def test_native_missing_feature_counts_only_an_emitted_policy_action(self):
+        for recurrent in (False, True):
+            with self.subTest(recurrent=recurrent):
+                model = self.model()
+                model.recurrent = recurrent
+                model.freqai_info["fit_live_predictions_candles"] = 3
+                pair = "BTC/USDT"
+                dk = FreqaiDataKitchen(model.config, live=True, pair=pair)
+                dk.label_list = ["&-action"]
+                dk.unique_class_list = []
+                dk.data["extra_returns_per_train"] = {}
+                dk.data["labels_mean"] = {"&-action": 0.0}
+                dk.data["labels_std"] = {"&-action": 0.0}
+                candles = pd.DataFrame(
+                    {
+                        "date": pd.date_range("2026-01-01", periods=2, freq="5min", tz="UTC"),
+                        "high": 101.0,
+                        "low": 99.0,
+                        "close": 100.0,
+                    }
+                )
+                model.set_initial_historic_predictions(
+                    pd.DataFrame({"&-action": [4.0]}), dk, pair, candles.iloc[:1]
+                )
+                model.dd.set_initial_return_values(
+                    pair, pd.DataFrame({"&-action": [4.0]}), candles.iloc[:1]
+                )
+                model.dd.append_model_predictions(
+                    pair, pd.DataFrame({"&-action": [4.0]}), np.array([1]), dk, candles.iloc[:1]
+                )
+                features, _ = dk.filter_features(
+                    pd.DataFrame({"%-feature": [np.nan]}, index=[29]),
+                    ["%-feature"],
+                    training_filter=False,
+                )
+                self.assertEqual(features.iloc[0, 0], 0.0)
+                policy = RecordingPolicy()
+                predictions = model.rl_model_predict(features, dk, policy)
+                model.dd.append_model_predictions(pair, predictions, dk.do_predict, dk, candles)
+                self.assertEqual(len(policy.observations), 0 if recurrent else 1)
+                self.assertEqual(dk.do_predict[-1], 0)
+                self.assertEqual(
+                    bool(
+                        model.dd.historic_predictions[pair]["_freqai_strategies_produced"].iloc[-1]
+                    ),
+                    not recurrent,
+                )
+                model.fit_live_predictions(dk, pair)
+                self.assertEqual(dk.data["labels_mean"]["&-action"], 4.0 if recurrent else 2.0)
+
+    def test_policy_production_tracks_calls_not_validity_or_neutral_action(self):
+        for recurrent in (False, True):
+            with self.subTest(recurrent=recurrent):
+                model = self.model()
+                model.live = False
+                model.recurrent = recurrent
+                model.CONV_WIDTH = 2
+                policy = RecordingPolicy()
+                calls = []
+                original_predict = policy.predict
+
+                def record(observation, calls=calls, original_predict=original_predict, **kwargs):
+                    calls.append(
+                        {
+                            key: value.copy() if isinstance(value, np.ndarray) else value
+                            for key, value in kwargs.items()
+                        }
+                    )
+                    return original_predict(observation, **kwargs)
+
+                policy.predict = record
+                dk = SimpleNamespace(
+                    pair="BTC/USDT",
+                    label_list=["&-action"],
+                    do_predict=np.array([1, 0, 1, 1]),
+                    data_dictionary={},
+                )
+                result = model.rl_model_predict(
+                    pd.DataFrame({"f": [10.0, 20.0, 30.0, 40.0]}, index=[7, 13, 29, 41]),
+                    dk,
+                    policy,
+                )
+                expected = [False, False, False, True] if recurrent else [False, True, True, True]
+                produced = dk.__dict__["_reforcexy_prediction_produced"]
+                np.testing.assert_array_equal(produced, expected)
+                self.assertEqual(len(policy.observations), sum(expected))
+                np.testing.assert_array_equal(result["&-action"].iloc[1:], [0, 0, 0])
+                self.assertEqual(result.index.tolist(), [7, 13, 29, 41])
+                if recurrent:
+                    self.assertIsNone(calls[0]["state"])
+                    np.testing.assert_array_equal(calls[0]["episode_start"], [True])
+                dk.do_predict = np.ones(1, dtype=int)
+                model.rl_model_predict(pd.DataFrame({"f": [1.0]}), dk, policy)
+                np.testing.assert_array_equal(
+                    dk.__dict__["_reforcexy_prediction_produced"], [False]
+                )
+                self.assertIsNot(dk.__dict__["_reforcexy_prediction_produced"], produced)
+                dk.do_predict = np.ones(2, dtype=int)
+                model.rl_model_predict(pd.DataFrame({"f": [np.nan, 1.0]}), dk, policy)
+                np.testing.assert_array_equal(
+                    dk.__dict__["_reforcexy_prediction_produced"], [False, False]
+                )
+                model.CONV_WIDTH = 0
+                dk.do_predict = np.ones(1, dtype=int)
+                result = model.rl_model_predict(pd.DataFrame({"f": [1.0]}), dk, policy)
+                self.assertTrue(result["&-action"].isna().all())
+                np.testing.assert_array_equal(
+                    dk.__dict__["_reforcexy_prediction_produced"], [False]
+                )
+                self.assertEqual(len(policy.observations), sum(expected))
+
     def test_live_mask_uses_open_position_without_state_features(self):
         model = self.model()
         policy = RecordingPolicy()
@@ -567,6 +678,9 @@ class ReviewContractsTest(QaTestCase):
         )
         np.testing.assert_array_equal(prediction["&-action"].iloc[1:], [1, 1, 1])
         np.testing.assert_array_equal(dk.do_predict, [0, 0, 0, 1])
+        np.testing.assert_array_equal(
+            dk.__dict__["_reforcexy_prediction_produced"], [False, True, True, True]
+        )
         self.assertTrue(policy.masks[1][1])
         self.assertTrue(policy.masks[2][1])
         np.testing.assert_array_equal(policy.observations[1], [[[20.0], [30.0]]])
index c57fee94f84f5d2a9ca5c8f5ac75f719917a337f..0b6221f07cbf563b9bb2462ee1fbc4d1a9dbd2e7 100644 (file)
@@ -284,7 +284,7 @@ class TrainingObservationsTest(QaTestCase):
                     "close_price": [100.0],
                 }
             )
-            candles = pd.DataFrame({"date": [date]})
+            candles = pd.DataFrame({"date": [date, date + pd.Timedelta(minutes=5)]})
             drawer.set_initial_return_values(pair, pd.DataFrame({"&-action": [99.0]}), candles)
             self.assertNotIn("_freqai_strategies_produced", drawer.historic_predictions[pair])
             self.assertNotIn(
@@ -319,11 +319,23 @@ class TrainingObservationsTest(QaTestCase):
 
             # Native same-candle append overwrites a bootstrap candle. A rejected
             # prediction is nevertheless produced and must enter the statistics.
+            dk.__dict__["_reforcexy_prediction_produced"] = np.array([True])
             model.dd.append_model_predictions(
                 pair, pd.DataFrame({"&-action": [3.0]}), np.array([0]), dk, strat_df
             )
             model.fit_live_predictions(dk, pair)
             self.assertEqual(dk.data["labels_mean"]["&-action"], 3.0)
+            dk.__dict__["_reforcexy_prediction_produced"] = np.array([False])
+            model.dd.append_model_predictions(
+                pair, pd.DataFrame({"&-action": [0.0]}), np.array([0]), dk, strat_df
+            )
+            model.fit_live_predictions(dk, pair)
+            self.assertEqual(dk.data["labels_mean"]["&-action"], 0.0)
+            self.assertFalse(model.dd.historic_predictions[pair][marker].iloc[-1])
+            dk.__dict__["_reforcexy_prediction_produced"] = np.array([True])
+            model.dd.append_model_predictions(
+                pair, pd.DataFrame({"&-action": [3.0]}), np.array([0]), dk, strat_df
+            )
             self.assertNotIn(
                 marker, model.dd.attach_return_values_to_return_dataframe(pair, strat_df)
             )
@@ -364,6 +376,13 @@ class TrainingObservationsTest(QaTestCase):
             self.assertEqual(len(model.dd.historic_predictions[pair]), len(resumed))
             model.fit_live_predictions(dk, pair)
             self.assertEqual(dk.data["labels_mean"]["&-action"], 6.0)
+            before_older = model.dd.historic_predictions[pair].copy(deep=True)
+            dk.__dict__["_reforcexy_prediction_produced"] = np.array([False])
+            model.dd.append_model_predictions(
+                pair, pd.DataFrame({"&-action": [99.0]}), np.array([0]), dk, strat_df
+            )
+            pd.testing.assert_frame_equal(model.dd.historic_predictions[pair], before_older)
+            dk.__dict__["_reforcexy_prediction_produced"] = np.array([True])
 
             model.dd.save_historic_predictions_to_disk()
             restored = ReforceXY(config=config)
@@ -379,6 +398,173 @@ class TrainingObservationsTest(QaTestCase):
                 marker, restored.dd.attach_return_values_to_return_dataframe(pair, resumed)
             )
 
+    def test_native_schema_extension_and_restart_preserve_unknown_metadata(self):
+        pair = "BTC/USDT"
+        marker = "_freqai_strategies_produced"
+        with tempfile.TemporaryDirectory() as temp:
+            config = model_config(temp)
+            config["freqai"]["fit_live_predictions_candles"] = 6
+            model = ReforceXY(config=config)
+            model.live = True
+            self.addCleanup(model.close_envs)
+            dates = pd.date_range("2026-01-01", periods=6, freq="5min", tz="UTC")
+            candles = pd.DataFrame(
+                {
+                    "date": dates,
+                    "high": 101.0,
+                    "low": 99.0,
+                    "close": 100.0,
+                }
+            )
+            model.dd.historic_predictions[pair] = pd.DataFrame(
+                {
+                    "date_pred": dates[:4],
+                    "&-action": [0.0, 7.0, 99.0, 88.0],
+                    "do_predict": [0, 1, 0, 2],
+                    "high_price": 101.0,
+                    "low_price": 99.0,
+                    "close_price": 100.0,
+                    marker: [True, False, np.nan, np.nan],
+                    "existing_metric": [0.0, np.nan, 2.0, 3.0],
+                }
+            )
+            dk = FreqaiDataKitchen(config, live=True, pair=pair)
+            dk.label_list = ["&-action", "&-new_label"]
+            dk.unique_class_list = ["class_probability"]
+            dk.data["labels_mean"] = dict.fromkeys(dk.label_list + dk.unique_class_list, 0.0)
+            dk.data["labels_std"] = dict.fromkeys(dk.label_list + dk.unique_class_list, 0.0)
+            dk.data["extra_returns_per_train"] = {
+                "new_metric": 12.0,
+                "python_flag": True,
+                "numpy_flag": np.bool_(False),
+            }
+            dk.__dict__["_reforcexy_prediction_produced"] = np.array([True])
+            model.dd.append_model_predictions(
+                pair,
+                pd.DataFrame(
+                    {
+                        "&-action": [0.0],
+                        "&-new_label": [2.0],
+                        "class_probability": [0.75],
+                    }
+                ),
+                np.array([0]),
+                dk,
+                candles.iloc[:5],
+            )
+            history = model.dd.historic_predictions[pair]
+            for column in (
+                "new_metric",
+                "python_flag",
+                "numpy_flag",
+                "&-action_mean",
+                "&-action_std",
+                "&-new_label",
+                "&-new_label_mean",
+                "&-new_label_std",
+                "class_probability",
+                "class_probability_mean",
+                "class_probability_std",
+            ):
+                self.assertTrue(history[column].iloc[:4].isna().all(), column)
+            self.assertEqual(history["new_metric"].iloc[-1], 12.0)
+            self.assertNotIn("DI_values", history)
+            self.assertIsInstance(history["python_flag"].iloc[-1], (bool, np.bool_))
+            self.assertIsInstance(history["numpy_flag"].iloc[-1], (bool, np.bool_))
+            self.assertEqual(history["python_flag"].dtype, object)
+            self.assertEqual(history["numpy_flag"].dtype, object)
+            self.assertEqual(history[marker].iloc[:2].tolist(), [True, False])
+            self.assertTrue(pd.isna(history[marker].iloc[2]))
+            self.assertFalse(history[marker].iloc[3])
+            model.dd.save_historic_predictions_to_disk()
+            restarted = ReforceXY(config=config)
+            restarted.live = True
+            self.addCleanup(restarted.close_envs)
+            restarted.dd.set_initial_return_values(
+                pair, pd.DataFrame({"&-action": np.zeros(6)}), candles
+            )
+            restored = restarted.dd.historic_predictions[pair]
+            self.assertTrue(restored["new_metric"].iloc[:4].isna().all())
+            self.assertEqual(restored["new_metric"].iloc[4], 12.0)
+            for column, expected in (("python_flag", True), ("numpy_flag", False)):
+                self.assertTrue(restored[column].iloc[:4].isna().all())
+                self.assertEqual(restored[column].dtype, object)
+                self.assertIsInstance(restored[column].iloc[4], (bool, np.bool_))
+                self.assertEqual(restored[column].iloc[4], expected)
+            self.assertEqual(restored["existing_metric"].iloc[0], 0.0)
+            self.assertTrue(pd.isna(restored["existing_metric"].iloc[1]))
+            self.assertTrue(pd.isna(restored[marker].iloc[2]))
+            self.assertEqual(
+                restored[marker].iloc[[0, 1, 3, 4, 5]].tolist(), [True, False, False, True, False]
+            )
+            restarted.fit_live_predictions(dk, pair)
+            self.assertEqual(dk.data["labels_mean"]["&-action"], 0.0)
+
+    def test_native_expiration_ignores_stale_production_and_resume_records_real_zero(self):
+        pair = "BTC/USDT"
+        marker = "_freqai_strategies_produced"
+        with tempfile.TemporaryDirectory() as temp:
+            config = model_config(temp)
+            config["freqai"]["fit_live_predictions_candles"] = 3
+            model = ReforceXY(config=config)
+            model.live = True
+            model.can_short = False
+            model.get_state_info = lambda pair: (0.5, 0.0, 0)
+            self.addCleanup(model.close_envs)
+            dk = FreqaiDataKitchen(config, live=True, pair=pair)
+            dk.label_list = ["&-action"]
+            dk.unique_class_list = []
+            dk.data["extra_returns_per_train"] = {}
+            dates = pd.date_range("2026-01-01", periods=3, freq="5min", tz="UTC")
+            candles = pd.DataFrame(
+                {
+                    "date": dates,
+                    "high": 101.0,
+                    "low": 99.0,
+                    "close": 100.0,
+                }
+            )
+            model.set_initial_historic_predictions(
+                pd.DataFrame({"&-action": [0.0]}), dk, pair, candles.iloc[:1]
+            )
+            model.dd.set_initial_return_values(
+                pair, pd.DataFrame({"&-action": [0.0]}), candles.iloc[:1]
+            )
+            dk.data["labels_mean"] = {"&-action": 0.0}
+            dk.data["labels_std"] = {"&-action": 0.0}
+            model.dd.append_model_predictions(
+                pair, pd.DataFrame({"&-action": [4.0]}), np.array([0]), dk, candles.iloc[:1]
+            )
+            dk.__dict__["_reforcexy_prediction_produced"] = np.ones(7, dtype=bool)
+            config["freqai"]["expiration_hours"] = 1
+            model.dk = dk
+            with mock.patch.object(
+                model, "predict", side_effect=AssertionError("expired inference")
+            ):
+                model.build_strategy_return_arrays(candles.iloc[:2], dk, pair, 0)
+            self.assertEqual(dk.return_dataframe["do_predict"].iloc[-1], 2)
+            self.assertEqual(dk.return_dataframe["&-action"].iloc[-1], 0.0)
+            self.assertFalse(model.dd.historic_predictions[pair][marker].iloc[-1])
+            model.fit_live_predictions(dk, pair)
+            self.assertEqual(dk.data["labels_mean"]["&-action"], 4.0)
+
+            def resume(frame, kitchen, **kwargs):
+                policy = RecordingPolicy()
+                kitchen.do_predict = np.zeros(len(frame), dtype=int)
+                prediction = model.rl_model_predict(pd.DataFrame({"f": [1.0]}), kitchen, policy)
+                self.assertEqual(len(policy.observations), 1)
+                return prediction, kitchen.do_predict
+
+            with (
+                mock.patch.object(dk, "check_if_model_expired", return_value=False),
+                mock.patch.object(model, "predict", side_effect=resume),
+            ):
+                model.build_strategy_return_arrays(candles, dk, pair, 0)
+            self.assertTrue(model.dd.historic_predictions[pair][marker].iloc[-1])
+            self.assertEqual(model.dd.historic_predictions[pair]["&-action"].iloc[-1], 0.0)
+            model.fit_live_predictions(dk, pair)
+            self.assertEqual(dk.data["labels_mean"]["&-action"], 2.0)
+
     def test_partially_marked_history_restores_provable_observations(self):
         pair = "BTC/USDT"
         marker = "_freqai_strategies_produced"
index 2c38f113bee6042e45f672e7116501c63448734b..5c5f54ddbc92b84debddea5bbbe86bc41aaf8e31 100644 (file)
@@ -1,6 +1,7 @@
 """Strategy signal, validation and sizing contracts; requires the Freqtrade QA image."""
 
 import unittest
+import warnings
 from datetime import datetime, timezone
 from unittest import mock
 
@@ -335,6 +336,34 @@ class FeatureEngineeringTest(QaTestCase):
             equal_nan=True,
         )
 
+    def test_invalid_close_breaks_returns_until_two_valid_prices_resume(self):
+        for invalid in (0.0, -1.0, np.nan, np.inf, -np.inf):
+            with self.subTest(invalid=invalid):
+                frame = _frame([0] * 4)
+                frame.index = [7, 13, 29, 41]
+                frame["close"] = [100.0, invalid, 110.0, 121.0]
+                original = frame.copy(deep=True)
+                with warnings.catch_warnings():
+                    warnings.simplefilter("error", RuntimeWarning)
+                    result = self._strategy().feature_engineering_expand_basic(frame, {})
+                pd.testing.assert_frame_equal(result[original.columns], original)
+                pd.testing.assert_series_equal(
+                    result["%-raw_volume"], original["volume"], check_names=False
+                )
+                np.testing.assert_allclose(
+                    result["%-close_log_return"],
+                    [np.nan, np.nan, np.nan, np.log(1.1)],
+                    equal_nan=True,
+                )
+
+    def test_empty_feature_frame_keeps_its_index_and_columns(self):
+        frame = _frame([]).astype({"close": float, "volume": float})
+        result = self._strategy().feature_engineering_expand_basic(frame, {})
+        self.assertTrue(result.empty)
+        self.assertTrue(result.index.equals(frame.index))
+        self.assertIn("%-close_log_return", result)
+        self.assertIn("%-raw_volume", result)
+
     def test_the_target_column_is_the_neutral_action(self):
         result = self._strategy().set_freqai_targets(
             _frame([Actions.Long_enter.value, Actions.Short_enter.value]), {}
index e9b039f1bbc807af69d682e117641d336748bd60..87855fa79390efe009e67c81b20a10fa413a5c02 100644 (file)
@@ -5,6 +5,8 @@ import random
 import sys
 import unittest
 from pathlib import Path
+from types import ModuleType
+from unittest import mock
 
 import numpy as np
 import torch
@@ -73,15 +75,26 @@ class SuiteContractTest(unittest.TestCase):
                 )
 
     def test_no_measured_file_is_registered_under_a_bare_stem(self):
-        # CI puts `user_data/strategies` on PYTHONPATH, so a bare `import RLAgentStrategy`
-        # would resolve. A dotted name is the only one the production import uses.
-        for name, _ in _source_files_under(MEASURED_TREE):
+        expected = {
+            Path(importlib.import_module(name).__file__).resolve(): name
+            for name in PRODUCTION_NAMES
+        }
+        for name, path in _source_files_under(MEASURED_TREE):
             with self.subTest(module=name):
-                self.assertTrue(
-                    name.startswith("ReforceXY.user_data."),
-                    "a measured file registered under a bare stem has a second module object, "
-                    "and every process-global restore would clean the copy under test never uses",
-                )
+                self.assertEqual(expected[path], name)
+
+    def test_file_identity_guard_rejects_strategy_and_model_aliases(self):
+        for canonical in PRODUCTION_NAMES:
+            with self.subTest(module=canonical):
+                module = ModuleType("arbitrary_production_alias")
+                module.__file__ = importlib.import_module(canonical).__file__
+                result = unittest.TestResult()
+                with mock.patch.dict(sys.modules, {module.__name__: module}):
+                    SuiteContractTest(
+                        "test_each_measured_source_file_is_registered_under_exactly_one_name"
+                    ).run(result)
+                self.assertEqual(result.errors, [])
+                self.assertEqual(len(result.failures), 1)
 
     def test_every_test_module_is_registered_under_its_bare_stem(self):
         # The half that catches a shadow copy of the test tree itself. A measured-tree walk
index 7e6711b4c0464bcc3f5cc8d7f7f7e65b1707a373..4c51bfec7d3e2ff6511b169d493d64f0cae0d2f7 100644 (file)
@@ -155,18 +155,20 @@ def _produced_prediction_mask(frame: pd.DataFrame) -> NDArray[np.bool_]:
 
 
 def _ensure_prediction_provenance(frame: pd.DataFrame) -> pd.DataFrame:
-    """Preserve explicit markers and infer only distinguishable legacy predictions."""
+    """Infer proven legacy outputs without turning ambiguous zeros into placeholders."""
+    inferred = pd.Series(np.nan, index=frame.index, dtype=object)
+    inferred.loc[_legacy_produced_prediction_mask(frame)] = True
+    if "do_predict" in frame:
+        inferred.loc[pd.to_numeric(frame["do_predict"], errors="coerce").eq(2)] = False
     if _PRODUCED_COLUMN not in frame:
         result = frame.copy()
-        result[_PRODUCED_COLUMN] = _legacy_produced_prediction_mask(frame)
+        result[_PRODUCED_COLUMN] = inferred
         return result
-    missing = frame[_PRODUCED_COLUMN].isna()
+    missing = frame[_PRODUCED_COLUMN].isna() & inferred.notna()
     if not missing.any():
         return frame
     result = frame.copy()
-    result[_PRODUCED_COLUMN] = frame[_PRODUCED_COLUMN].where(
-        ~missing, _legacy_produced_prediction_mask(frame)
-    )
+    result[_PRODUCED_COLUMN] = frame[_PRODUCED_COLUMN].where(~missing, inferred)
     return result
 
 
@@ -219,6 +221,8 @@ def _align_historic_predictions(history: pd.DataFrame, dataframe: pd.DataFrame)
         aligned["do_predict"] = aligned["do_predict"].where(~missing, 0)
     else:
         aligned["do_predict"] = 0
+    if _PRODUCED_COLUMN in aligned:
+        aligned[_PRODUCED_COLUMN] = aligned[_PRODUCED_COLUMN].where(~missing, False)
     return aligned
 
 
@@ -628,6 +632,29 @@ class ReforceXY(BaseReinforcementLearningModel):
         self._install_replay_persistence()
         self._install_prediction_provenance()
 
+    def _ensure_history_schema(self, dk: FreqaiDataKitchen, pair: str) -> None:
+        """Extend restored history without inventing past observations or metadata."""
+        if pair not in self.dd.historic_predictions:
+            return
+        history = _ensure_prediction_provenance(self.dd.historic_predictions[pair])
+        columns = dict.fromkeys(dk.label_list + dk.unique_class_list, np.nan)
+        for label in dk.label_list + dk.unique_class_list:
+            columns[f"{label}_mean"] = np.nan
+            columns[f"{label}_std"] = np.nan
+        columns.update(dk.data.get("extra_returns_per_train", {}))
+        if self.ft_params.get("DI_threshold", 0) > 0:
+            columns["DI_values"] = np.nan
+        for column, value in columns.items():
+            if column not in history:
+                dtype = (
+                    float
+                    if isinstance(value, (int, float, np.number))
+                    and not isinstance(value, (bool, np.bool_))
+                    else object
+                )
+                history[column] = pd.Series(np.nan, index=history.index, dtype=dtype)
+        self.dd.historic_predictions[pair] = history
+
     def _install_prediction_provenance(self) -> None:
         """Keep live observation metadata local to this model's drawer instance."""
         drawer = self.dd
@@ -651,13 +678,17 @@ class ReforceXY(BaseReinforcementLearningModel):
         def set_initial_with_provenance(
             pair: str, pred_df: pd.DataFrame, dataframe: pd.DataFrame
         ) -> None:
-            drawer.historic_predictions[pair] = _ensure_prediction_provenance(
-                drawer.historic_predictions[pair]
+            history = _dedupe_historic_predictions_on_date_pred(
+                _ensure_prediction_provenance(drawer.historic_predictions[pair]), pair
             )
+            drawer.historic_predictions[pair] = history
+            missing = history.set_index("date_pred").isna()
             set_initial(pair, pred_df, dataframe)
-            drawer.historic_predictions[pair] = _ensure_prediction_provenance(
-                drawer.historic_predictions[pair]
-            )
+            result = drawer.historic_predictions[pair]
+            old_missing = missing.reindex(pd.DatetimeIndex(result["date_pred"]), fill_value=False)
+            old_missing.index = result.index
+            result[missing.columns] = result[missing.columns].mask(old_missing)
+            result.loc[~result["date_pred"].isin(missing.index), _PRODUCED_COLUMN] = False
             drawer.model_return_values[pair] = _align_historic_predictions(
                 drawer.historic_predictions[pair], dataframe
             )
@@ -670,11 +701,11 @@ class ReforceXY(BaseReinforcementLearningModel):
             dk: FreqaiDataKitchen,
             strat_df: pd.DataFrame,
         ) -> None:
-            drawer.historic_predictions[pair] = _ensure_prediction_provenance(
-                drawer.historic_predictions[pair]
-            )
+            self._ensure_history_schema(dk, pair)
+            old_dates = pd.DatetimeIndex(drawer.historic_predictions[pair]["date_pred"])
             append(pair, predictions, do_preds, dk, strat_df)
             history = drawer.historic_predictions[pair]
+            history.loc[~history["date_pred"].isin(old_dates), _PRODUCED_COLUMN] = False
             # The native writer may return without writing an older candle.
             if (
                 not history.empty
@@ -682,7 +713,10 @@ class ReforceXY(BaseReinforcementLearningModel):
                 and pd.to_datetime(history["date_pred"].iloc[-1], utc=True)
                 == pd.to_datetime(strat_df["date"].iloc[-1], utc=True)
             ):
-                history.loc[history.index[-1], _PRODUCED_COLUMN] = True
+                produced = dk.__dict__.get("_reforcexy_prediction_produced")
+                history.at[history.index[-1], _PRODUCED_COLUMN] = bool(
+                    do_preds[-1] != 2 and (produced is None or produced[-1])
+                )
             drawer.model_return_values[pair] = _align_historic_predictions(history, strat_df)
 
         @wraps(attach)
@@ -1750,6 +1784,7 @@ class ReforceXY(BaseReinforcementLearningModel):
         self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs
     ) -> tuple[DataFrame, NDArray[np.int_]]:
         """Feed source dates to inference; parent predict performs the filtering."""
+        dk.__dict__.pop("_reforcexy_prediction_produced", None)
         dk.data_dictionary["prediction_dates"] = unfiltered_df[["date"]]
         try:
             return super().predict(unfiltered_df, dk, **kwargs)
@@ -1774,6 +1809,8 @@ class ReforceXY(BaseReinforcementLearningModel):
             virtual_trade_duration = trade_duration
         np_dataframe: NDArray[np.float32] = dataframe.to_numpy(dtype=np.float32, copy=False)
         n = np_dataframe.shape[0]
+        produced = np.zeros(n, dtype=bool)
+        dk.__dict__["_reforcexy_prediction_produced"] = produced
         window_size: int = self.CONV_WIDTH
         frame_stacking: int = self.frame_stacking
         frame_stacking_enabled: bool = bool(frame_stacking) and frame_stacking > 1
@@ -1979,6 +2016,7 @@ class ReforceXY(BaseReinforcementLearningModel):
                     action,
                 )
 
+            produced[end_idx - 1] = True
             return action
 
         predicted_actions: list[int] = []
index 759e05cef2d51bdf25027efa516a356ee099a20d..e6c53d81237f7e01dbc3ccafca3bc6164049ba55 100644 (file)
@@ -87,7 +87,18 @@ class RLAgentStrategy(IStrategy):
     def feature_engineering_expand_basic(
         self, dataframe: DataFrame, metadata: dict[str, Any], **kwargs
     ) -> DataFrame:
-        dataframe["%-close_log_return"] = np.log(dataframe.get("close")).diff()
+        close_values = dataframe["close"].to_numpy(dtype=float)
+        valid = np.isfinite(close_values) & (close_values > 0.0)
+        invalid_close_count = int(np.count_nonzero(~valid))
+        if invalid_close_count:
+            logger.debug(
+                "feature_engineering_expand_basic: %d close values are non-finite or non-positive; close log return is NaN at those positions",
+                invalid_close_count,
+            )
+        with np.errstate(divide="ignore", invalid="ignore"):
+            dataframe["%-close_log_return"] = pd.Series(
+                np.where(valid, np.log(close_values), np.nan), index=dataframe.index
+            ).diff()
         dataframe["%-raw_volume"] = dataframe.get("volume")
 
         return dataframe
index 5995fc2a112908310e095c220bad5967963e5dfb..ab9c6fa638fa65099bbf3b89b9252ab66468a9f3 100644 (file)
@@ -9,22 +9,31 @@ procedure does not establish that the current defaults are optimal.
 
 ## What the backtest measures
 
-In Freqtrade 2026.8, the [native backtest][freqai-running] constructs each pair's
-rolling predictions before replaying enabled `fit_live_predictions()` updates
-([training loop][freqai-source], [replay loop][freqai-replay]). It exercises
-rolling model fits, threshold replay and strategy decisions, but a label-HPO
-update during replay cannot affect an already-trained model. Testing that live
-feedback requires a chronological runner that interleaves training, prediction
-and state updates, or a forward dry-run. This repository provides no such runner;
-do not present native-backtest results as validation of the complete live loop.
-
-QuickAdapter predicts smoothed Zigzag morphology, not returns. `holdout_rmse`
-measures the selection model's weighted error on the original label scale,
-on a holdout within the training window, before any deployment refit. An empty
-holdout, including one emptied by causal purging, yields `holdout_rmse=inf`
-(unavailable). With `method=train_test_split`, `test_size=0` disables internal
-validation and final refit, not later rolling predictions; `timeseries_split`
-does not accept zero. Use RMSE to diagnose prediction quality, not profitability.
+The [native FreqAI backtest][freqai-running] runs in two phases:
+
+1. Train models and construct each pair's rolling predictions
+   ([training loop][freqai-source]).
+2. Replay enabled `fit_live_predictions()` updates over those predictions
+   ([replay loop][freqai-replay]).
+
+QuickAdapter replay follows the same observation and gap-warmup rules as live
+calibration; see its [prediction contract](../quickadapter/README.md#prediction-warmup-and-restarts).
+Label HPO reads OHLCV only through the current replay candle, even when
+predictions are missing. Its parameter changes feed the strategy's per-candle
+NATR and distance calculations. Model RMSE remains tied to its original training
+window.
+
+**Limit:** replay cannot change models trained in the first phase. Evaluating
+the complete live feedback loop requires a chronological runner that interleaves
+training, prediction and state updates, or a forward dry-run. This repository
+provides no such runner. Do not present native-backtest results as validation of
+that complete loop.
+
+Use [QuickAdapter's model-error guidance](../quickadapter/README.md#model-error-and-recovery)
+to interpret RMSE and unavailable scores; neither establishes profitability.
+With `method=train_test_split`, `test_size=0` disables internal validation and
+deployment refit, not later rolling predictions. `timeseries_split` does not
+accept zero.
 
 ## Design the comparison
 
@@ -43,10 +52,11 @@ does not accept zero. Use RMSE to diagnose prediction quality, not profitability
    Threshold calibration must use only predictions available at that time.
 3. **Respect label availability.** Keep `causal_mode` enabled. A historical row
    is not usable for training until all observations needed for its labels and
-   weights are known. Add each `known_at_lookahead` candle offset to its row
-   position in the unsliced window; use the latest availability across labels
-   and weights. Audit it against each split cutoff, rejecting unknown or
-   out-of-frame availability. `causal_mode` alone is not proof of this invariant.
+   weights are known. Compare each `known_at_lookahead` candle offset with the
+   cutoff minus its row position in the unsliced window; use the latest
+   availability across labels and weights. Reject unknown, invalid or
+   out-of-frame availability per row, not by discarding an entire availability
+   column. `causal_mode` alone is not proof of this invariant.
    Allow for additional publication/execution delays where relevant. Purging
    removes overlapping label information; an embargo excludes training samples
    immediately _after_ a validation block when a split uses future training data
index a32e345ff81fd318472d1d30ecbd59bd49f3eff4..aa1458d9f12e1c538feccb16104557b866b99091 100644 (file)
   "diagnostics": [
     {
       "endCharacter": 46,
-      "endLine": 1402,
+      "endLine": 1438,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"str | None\" cannot be assigned to parameter \"default\" of type \"str\" in function \"_validate_label_selection_metric\"\n  Type \"str | None\" is not assignable to type \"str\"\n    \"None\" is not assignable to \"str\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 24,
-      "startLine": 1402
+      "startLine": 1438
     },
     {
       "endCharacter": 79,
-      "endLine": 2265,
+      "endLine": 2808,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"index\" for class \"NDArray[Unknown]\"\n  Attribute \"index\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 74,
-      "startLine": 2265
+      "startLine": 2808
     },
     {
       "endCharacter": 72,
-      "endLine": 2267,
+      "endLine": 2810,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"index\" for class \"NDArray[Unknown]\"\n  Attribute \"index\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 67,
-      "startLine": 2267
+      "startLine": 2810
     },
     {
       "endCharacter": 90,
-      "endLine": 2276,
+      "endLine": 2819,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"index\" for class \"NDArray[Unknown]\"\n  Attribute \"index\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 85,
-      "startLine": 2276
+      "startLine": 2819
     },
     {
       "endCharacter": 34,
-      "endLine": 2289,
+      "endLine": 2831,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Unknown | NDArray[Unknown] | Any | list[Unknown]\" cannot be assigned to parameter \"train_features\" of type \"DataFrame\" in function \"_filter_train_by_mask\"\n  Type \"Unknown | NDArray[Unknown] | Any | list[Unknown]\" is not assignable to type \"DataFrame\"\n    \"list[Unknown]\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 20,
-      "startLine": 2289
+      "startLine": 2831
     },
     {
       "endCharacter": 32,
-      "endLine": 2290,
+      "endLine": 2832,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Unknown | NDArray[Unknown] | Any | list[Unknown]\" cannot be assigned to parameter \"train_labels\" of type \"DataFrame\" in function \"_filter_train_by_mask\"\n  Type \"Unknown | NDArray[Unknown] | Any | list[Unknown]\" is not assignable to type \"DataFrame\"\n    \"list[Unknown]\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 20,
-      "startLine": 2290
+      "startLine": 2832
     },
     {
       "endCharacter": 38,
-      "endLine": 2291,
+      "endLine": 2833,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Unknown | NDArray[Unknown] | Any | list[Unknown]\" cannot be assigned to parameter \"train_weights\" of type \"NDArray[floating[Any]]\" in function \"_filter_train_by_mask\"\n  Type \"Unknown | NDArray[Unknown] | Any | list[Unknown]\" is not assignable to type \"NDArray[floating[Any]]\"\n    \"list[Unknown]\" is not assignable to \"ndarray[_AnyShape, dtype[floating[Any]]]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 20,
-      "startLine": 2291
+      "startLine": 2833
     },
     {
       "endCharacter": 59,
-      "endLine": 2294,
+      "endLine": 2836,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Unknown | NDArray[Unknown] | Any | list[Unknown] | None\" cannot be assigned to parameter \"train_label_weights\" of type \"NDArray[floating[Any]] | None\" in function \"_filter_train_by_mask\"\n  Type \"Unknown | NDArray[Unknown] | Any | list[Unknown] | None\" is not assignable to type \"NDArray[floating[Any]] | None\"\n    Type \"list[Unknown]\" is not assignable to type \"NDArray[floating[Any]] | None\"\n      \"list[Unknown]\" is not assignable to \"ndarray[_AnyShape, dtype[floating[Any]]]\"\n      \"list[Unknown]\" is not assignable to \"None\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 40,
-      "startLine": 2294
+      "startLine": 2836
     },
     {
       "endCharacter": 34,
-      "endLine": 2328,
+      "endLine": 2870,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"DataFrame | Unknown | NDArray[Unknown] | Any | list[Unknown]\" cannot be assigned to parameter \"features\" of type \"DataFrame\" in function \"_shuffle_split_rows\"\n  Type \"DataFrame | Unknown | NDArray[Unknown] | Any | list[Unknown]\" is not assignable to type \"DataFrame\"\n    \"list[Unknown]\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 20,
-      "startLine": 2328
+      "startLine": 2870
     },
     {
       "endCharacter": 32,
-      "endLine": 2329,
+      "endLine": 2871,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"DataFrame | Unknown | NDArray[Unknown] | Any | list[Unknown]\" cannot be assigned to parameter \"labels\" of type \"DataFrame\" in function \"_shuffle_split_rows\"\n  Type \"DataFrame | Unknown | NDArray[Unknown] | Any | list[Unknown]\" is not assignable to type \"DataFrame\"\n    \"list[Unknown]\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 20,
-      "startLine": 2329
+      "startLine": 2871
     },
     {
       "endCharacter": 38,
-      "endLine": 2330,
+      "endLine": 2872,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"NDArray[floating[Any]] | Unknown | NDArray[Unknown] | Any | list[Unknown]\" cannot be assigned to parameter \"base_weights\" of type \"NDArray[floating[Any]]\" in function \"_shuffle_split_rows\"\n  Type \"NDArray[floating[Any]] | Unknown | NDArray[Unknown] | Any | list[Unknown]\" is not assignable to type \"NDArray[floating[Any]]\"\n    \"list[Unknown]\" is not assignable to \"ndarray[_AnyShape, dtype[floating[Any]]]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 20,
-      "startLine": 2330
+      "startLine": 2872
     },
     {
       "endCharacter": 39,
-      "endLine": 2331,
+      "endLine": 2873,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"NDArray[floating[Any]] | Unknown | NDArray[Unknown] | Any | list[Unknown] | None\" cannot be assigned to parameter \"label_weights\" of type \"NDArray[floating[Any]] | None\" in function \"_shuffle_split_rows\"\n  Type \"NDArray[floating[Any]] | Unknown | NDArray[Unknown] | Any | list[Unknown] | None\" is not assignable to type \"NDArray[floating[Any]] | None\"\n    Type \"list[Unknown]\" is not assignable to type \"NDArray[floating[Any]] | None\"\n      \"list[Unknown]\" is not assignable to \"ndarray[_AnyShape, dtype[floating[Any]]]\"\n      \"list[Unknown]\" is not assignable to \"None\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 20,
-      "startLine": 2331
+      "startLine": 2873
     },
     {
       "endCharacter": 37,
-      "endLine": 2338,
+      "endLine": 2880,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Unknown | NDArray[Unknown] | Any | list[Unknown] | DataFrame\" cannot be assigned to parameter \"features\" of type \"DataFrame\" in function \"_shuffle_split_rows\"\n  Type \"Unknown | NDArray[Unknown] | Any | list[Unknown] | DataFrame\" is not assignable to type \"DataFrame\"\n    \"list[Unknown]\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 24,
-      "startLine": 2338
+      "startLine": 2880
     },
     {
       "endCharacter": 35,
-      "endLine": 2339,
+      "endLine": 2881,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Unknown | NDArray[Unknown] | Any | list[Unknown] | DataFrame\" cannot be assigned to parameter \"labels\" of type \"DataFrame\" in function \"_shuffle_split_rows\"\n  Type \"Unknown | NDArray[Unknown] | Any | list[Unknown] | DataFrame\" is not assignable to type \"DataFrame\"\n    \"list[Unknown]\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 24,
-      "startLine": 2339
+      "startLine": 2881
     },
     {
       "endCharacter": 41,
-      "endLine": 2340,
+      "endLine": 2882,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Unknown | NDArray[Unknown] | Any | list[Unknown] | ndarray[_AnyShape, dtype[floating[Any]]]\" cannot be assigned to parameter \"base_weights\" of type \"NDArray[floating[Any]]\" in function \"_shuffle_split_rows\"\n  Type \"Unknown | NDArray[Unknown] | Any | list[Unknown] | ndarray[_AnyShape, dtype[floating[Any]]]\" is not assignable to type \"NDArray[floating[Any]]\"\n    \"list[Unknown]\" is not assignable to \"ndarray[_AnyShape, dtype[floating[Any]]]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 24,
-      "startLine": 2340
+      "startLine": 2882
     },
     {
       "endCharacter": 42,
-      "endLine": 2341,
+      "endLine": 2883,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Unknown | NDArray[Unknown] | Any | list[Unknown] | ndarray[_AnyShape, dtype[floating[Any]]] | None\" cannot be assigned to parameter \"label_weights\" of type \"NDArray[floating[Any]] | None\" in function \"_shuffle_split_rows\"\n  Type \"Unknown | NDArray[Unknown] | Any | list[Unknown] | ndarray[_AnyShape, dtype[floating[Any]]] | None\" is not assignable to type \"NDArray[floating[Any]] | None\"\n    Type \"list[Unknown]\" is not assignable to type \"NDArray[floating[Any]] | None\"\n      \"list[Unknown]\" is not assignable to \"ndarray[_AnyShape, dtype[floating[Any]]]\"\n      \"list[Unknown]\" is not assignable to \"None\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 24,
-      "startLine": 2341
+      "startLine": 2883
     },
     {
       "endCharacter": 30,
-      "endLine": 2359,
+      "endLine": 2901,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"NDArray[floating[Any]] | Unknown | NDArray[Unknown] | Any | list[Unknown]\" cannot be assigned to parameter \"base_weights\" of type \"NDArray[floating[Any]]\" in function \"_compose_train_weights_with_support\"\n  Type \"NDArray[floating[Any]] | Unknown | NDArray[Unknown] | Any | list[Unknown]\" is not assignable to type \"NDArray[floating[Any]]\"\n    \"list[Unknown]\" is not assignable to \"ndarray[_AnyShape, dtype[floating[Any]]]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 2359
+      "startLine": 2901
     },
     {
       "endCharacter": 31,
-      "endLine": 2360,
+      "endLine": 2902,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"NDArray[floating[Any]] | Unknown | NDArray[Unknown] | Any | list[Unknown] | None\" cannot be assigned to parameter \"label_weights\" of type \"NDArray[floating[Any]] | None\" in function \"_compose_train_weights_with_support\"\n  Type \"NDArray[floating[Any]] | Unknown | NDArray[Unknown] | Any | list[Unknown] | None\" is not assignable to type \"NDArray[floating[Any]] | None\"\n    Type \"list[Unknown]\" is not assignable to type \"NDArray[floating[Any]] | None\"\n      \"list[Unknown]\" is not assignable to \"ndarray[_AnyShape, dtype[floating[Any]]]\"\n      \"list[Unknown]\" is not assignable to \"None\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 2360
+      "startLine": 2902
     },
     {
       "endCharacter": 33,
-      "endLine": 2366,
+      "endLine": 2908,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"NDArray[floating[Any]] | Unknown | NDArray[Unknown] | Any | list[Unknown]\" cannot be assigned to parameter \"base_weights\" of type \"NDArray[floating[Any]]\" in function \"_compose_eval_weights\"\n  Type \"NDArray[floating[Any]] | Unknown | NDArray[Unknown] | Any | list[Unknown]\" is not assignable to type \"NDArray[floating[Any]]\"\n    \"list[Unknown]\" is not assignable to \"ndarray[_AnyShape, dtype[floating[Any]]]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 2366
+      "startLine": 2908
     },
     {
       "endCharacter": 34,
-      "endLine": 2367,
+      "endLine": 2909,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"NDArray[floating[Any]] | Unknown | NDArray[Unknown] | Any | list[Unknown] | None\" cannot be assigned to parameter \"label_weights\" of type \"NDArray[floating[Any]] | None\" in function \"_compose_eval_weights\"\n  Type \"NDArray[floating[Any]] | Unknown | NDArray[Unknown] | Any | list[Unknown] | None\" is not assignable to type \"NDArray[floating[Any]] | None\"\n    Type \"list[Unknown]\" is not assignable to type \"NDArray[floating[Any]] | None\"\n      \"list[Unknown]\" is not assignable to \"ndarray[_AnyShape, dtype[floating[Any]]]\"\n      \"list[Unknown]\" is not assignable to \"None\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 2367
+      "startLine": 2909
     },
     {
       "endCharacter": 26,
-      "endLine": 2374,
+      "endLine": 2916,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"DataFrame | Unknown | NDArray[Unknown] | Any | list[Unknown]\" cannot be assigned to parameter \"train_df\" of type \"DataFrame\" in function \"build_data_dictionary\"\n  Type \"DataFrame | Unknown | NDArray[Unknown] | Any | list[Unknown]\" is not assignable to type \"DataFrame\"\n    \"list[Unknown]\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 2374
+      "startLine": 2916
     },
     {
       "endCharacter": 25,
-      "endLine": 2375,
+      "endLine": 2917,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"DataFrame | Unknown | NDArray[Unknown] | Any | list[Unknown]\" cannot be assigned to parameter \"test_df\" of type \"DataFrame\" in function \"build_data_dictionary\"\n  Type \"DataFrame | Unknown | NDArray[Unknown] | Any | list[Unknown]\" is not assignable to type \"DataFrame\"\n    \"list[Unknown]\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 2375
+      "startLine": 2917
     },
     {
       "endCharacter": 24,
-      "endLine": 2376,
+      "endLine": 2918,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"DataFrame | Unknown | NDArray[Unknown] | Any | list[Unknown]\" cannot be assigned to parameter \"train_labels\" of type \"DataFrame\" in function \"build_data_dictionary\"\n  Type \"DataFrame | Unknown | NDArray[Unknown] | Any | list[Unknown]\" is not assignable to type \"DataFrame\"\n    \"list[Unknown]\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 2376
+      "startLine": 2918
     },
     {
       "endCharacter": 23,
-      "endLine": 2377,
+      "endLine": 2919,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"DataFrame | Unknown | NDArray[Unknown] | Any | list[Unknown]\" cannot be assigned to parameter \"test_labels\" of type \"DataFrame\" in function \"build_data_dictionary\"\n  Type \"DataFrame | Unknown | NDArray[Unknown] | Any | list[Unknown]\" is not assignable to type \"DataFrame\"\n    \"list[Unknown]\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 2377
+      "startLine": 2919
     },
     {
       "endCharacter": 87,
-      "endLine": 2703,
+      "endLine": 3249,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"index\" for class \"NDArray[Unknown]\"\n  Attribute \"index\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 82,
-      "startLine": 2703
+      "startLine": 3249
     },
     {
       "endCharacter": 68,
-      "endLine": 2704,
+      "endLine": 3250,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"index\" for class \"NDArray[Unknown]\"\n  Attribute \"index\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 63,
-      "startLine": 2704
+      "startLine": 3250
     },
     {
-      "endCharacter": 63,
-      "endLine": 2715,
+      "endCharacter": 71,
+      "endLine": 3259,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"index\" for class \"NDArray[Unknown]\"\n  Attribute \"index\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
-      "startCharacter": 58,
-      "startLine": 2715
+      "startCharacter": 66,
+      "startLine": 3259
     },
     {
       "endCharacter": 47,
-      "endLine": 2717,
+      "endLine": 3262,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"loc\" for class \"NDArray[Unknown]\"\n  Attribute \"loc\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 44,
-      "startLine": 2717
+      "startLine": 3262
     },
     {
       "endCharacter": 47,
-      "endLine": 2717,
+      "endLine": 3262,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"loc\" for class \"list[Unknown]\"\n  Attribute \"loc\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 44,
-      "startLine": 2717
+      "startLine": 3262
     },
     {
       "endCharacter": 43,
-      "endLine": 2718,
+      "endLine": 3263,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"loc\" for class \"NDArray[Unknown]\"\n  Attribute \"loc\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 40,
-      "startLine": 2718
+      "startLine": 3263
     },
     {
       "endCharacter": 43,
-      "endLine": 2718,
+      "endLine": 3263,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"loc\" for class \"list[Unknown]\"\n  Attribute \"loc\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 40,
-      "startLine": 2718
+      "startLine": 3263
     },
     {
       "endCharacter": 31,
-      "endLine": 2739,
+      "endLine": 3284,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"empty\" for class \"NDArray[Unknown]\"\n  Attribute \"empty\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 26,
-      "startLine": 2739
+      "startLine": 3284
     },
     {
       "endCharacter": 31,
-      "endLine": 2739,
+      "endLine": 3284,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"empty\" for class \"list[Unknown]\"\n  Attribute \"empty\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 26,
-      "startLine": 2739
+      "startLine": 3284
     },
     {
       "endCharacter": 60,
-      "endLine": 2739,
+      "endLine": 3284,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"empty\" for class \"NDArray[Unknown]\"\n  Attribute \"empty\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 55,
-      "startLine": 2739
+      "startLine": 3284
     },
     {
       "endCharacter": 60,
-      "endLine": 2739,
+      "endLine": 3284,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"empty\" for class \"list[Unknown]\"\n  Attribute \"empty\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 55,
-      "startLine": 2739
+      "startLine": 3284
     },
     {
       "endCharacter": 73,
-      "endLine": 2751,
+      "endLine": 3296,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Unknown | Any | ((value: Unknown, start: SupportsIndex = 0, stop: SupportsIndex = sys.maxsize, /) -> int)\" cannot be assigned to parameter \"target\" of type \"Index[Any]\" in function \"get_indexer\"\n  Type \"Unknown | Any | ((value: Unknown, start: SupportsIndex = 0, stop: SupportsIndex = sys.maxsize, /) -> int)\" is not assignable to type \"Index[Any]\"\n    \"MethodType\" is not assignable to \"Index[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 53,
-      "startLine": 2751
+      "startLine": 3296
     },
     {
       "endCharacter": 73,
-      "endLine": 2751,
+      "endLine": 3296,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"index\" for class \"NDArray[Unknown]\"\n  Attribute \"index\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 68,
-      "startLine": 2751
+      "startLine": 3296
     },
     {
       "endCharacter": 83,
-      "endLine": 2752,
+      "endLine": 3297,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Unknown | Any | ((value: Unknown, start: SupportsIndex = 0, stop: SupportsIndex = sys.maxsize, /) -> int)\" cannot be assigned to parameter \"target\" of type \"Index[Any]\" in function \"get_indexer\"\n  Type \"Unknown | Any | ((value: Unknown, start: SupportsIndex = 0, stop: SupportsIndex = sys.maxsize, /) -> int)\" is not assignable to type \"Index[Any]\"\n    \"MethodType\" is not assignable to \"Index[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 58,
-      "startLine": 2752
+      "startLine": 3297
     },
     {
       "endCharacter": 83,
-      "endLine": 2752,
+      "endLine": 3297,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"index\" for class \"NDArray[Unknown]\"\n  Attribute \"index\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 78,
-      "startLine": 2752
+      "startLine": 3297
     },
     {
       "endCharacter": 46,
-      "endLine": 2987,
+      "endLine": 3532,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"empty\" for class \"Buffer\"\n  Attribute \"empty\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 41,
-      "startLine": 2987
+      "startLine": 3532
     },
     {
       "endCharacter": 46,
-      "endLine": 2987,
+      "endLine": 3532,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"empty\" for class \"_NestedSequence[_SupportsArray[dtype[Any]]]\"\n  Attribute \"empty\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 41,
-      "startLine": 2987
+      "startLine": 3532
     },
     {
       "endCharacter": 46,
-      "endLine": 2987,
+      "endLine": 3532,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"empty\" for class \"_NestedSequence[complex | bytes | str]\"\n  Attribute \"empty\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 41,
-      "startLine": 2987
+      "startLine": 3532
     },
     {
       "endCharacter": 46,
-      "endLine": 2987,
+      "endLine": 3532,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"empty\" for class \"_SupportsArray[dtype[Any]]\"\n  Attribute \"empty\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 41,
-      "startLine": 2987
+      "startLine": 3532
     },
     {
       "endCharacter": 46,
-      "endLine": 2987,
+      "endLine": 3532,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"empty\" for class \"bytes\"\n  Attribute \"empty\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 41,
-      "startLine": 2987
+      "startLine": 3532
     },
     {
       "endCharacter": 46,
-      "endLine": 2987,
+      "endLine": 3532,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"empty\" for class \"complex\"\n  Attribute \"empty\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 41,
-      "startLine": 2987
+      "startLine": 3532
     },
     {
       "endCharacter": 46,
-      "endLine": 2987,
+      "endLine": 3532,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Cannot access attribute \"empty\" for class \"str\"\n  Attribute \"empty\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 41,
-      "startLine": 2987
+      "startLine": 3532
     },
     {
       "endCharacter": 41,
-      "endLine": 2994,
+      "endLine": 3539,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"ArrayLike\" cannot be assigned to parameter \"features\" of type \"DataFrame\" in function \"_sanitize_pipeline_weights\"\n  Type \"ArrayLike\" is not assignable to type \"DataFrame\"\n    \"Buffer\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 2994
+      "startLine": 3539
     },
     {
       "endCharacter": 39,
-      "endLine": 3043,
+      "endLine": 3588,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"ArrayLike\" cannot be assigned to parameter \"features\" of type \"DataFrame\" in function \"_sanitize_pipeline_weights\"\n  Type \"ArrayLike\" is not assignable to type \"DataFrame\"\n    \"Buffer\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 20,
-      "startLine": 3043
+      "startLine": 3588
     },
     {
       "endCharacter": 19,
-      "endLine": 3153,
+      "endLine": 3698,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"int | Any | None\" cannot be assigned to parameter \"gap\" of type \"int\" in function \"__init__\"\n  Type \"int | Any | None\" is not assignable to type \"int\"\n    \"None\" is not assignable to \"int\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 3153
+      "startLine": 3698
     },
     {
       "endCharacter": 21,
-      "endLine": 3265,
+      "endLine": 3809,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"X\" of type \"DataFrame\" in function \"hp_objective\"\n  Type \"Any | None\" is not assignable to type \"DataFrame\"\n    \"None\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 20,
-      "startLine": 3265
+      "startLine": 3809
     },
     {
       "endCharacter": 21,
-      "endLine": 3266,
+      "endLine": 3810,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"y\" of type \"DataFrame\" in function \"hp_objective\"\n  Type \"Any | None\" is not assignable to type \"DataFrame\"\n    \"None\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 20,
-      "startLine": 3266
+      "startLine": 3810
     },
     {
       "endCharacter": 33,
-      "endLine": 3267,
+      "endLine": 3811,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"train_weights\" of type \"NDArray[floating[Any]]\" in function \"hp_objective\"\n  Type \"Any | None\" is not assignable to type \"NDArray[floating[Any]]\"\n    \"None\" is not assignable to \"ndarray[_AnyShape, dtype[floating[Any]]]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 20,
-      "startLine": 3267
+      "startLine": 3811
     },
     {
       "endCharacter": 32,
-      "endLine": 3268,
+      "endLine": 3812,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"X_validation\" of type \"DataFrame\" in function \"hp_objective\"\n  Type \"Any | None\" is not assignable to type \"DataFrame\"\n    \"None\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 20,
-      "startLine": 3268
+      "startLine": 3812
     },
     {
       "endCharacter": 32,
-      "endLine": 3269,
+      "endLine": 3813,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"y_validation\" of type \"DataFrame\" in function \"hp_objective\"\n  Type \"Any | None\" is not assignable to type \"DataFrame\"\n    \"None\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 20,
-      "startLine": 3269
+      "startLine": 3813
     },
     {
       "endCharacter": 38,
-      "endLine": 3270,
+      "endLine": 3814,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"validation_weights\" of type \"NDArray[floating[Any]]\" in function \"hp_objective\"\n  Type \"Any | None\" is not assignable to type \"NDArray[floating[Any]]\"\n    \"None\" is not assignable to \"ndarray[_AnyShape, dtype[floating[Any]]]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 20,
-      "startLine": 3270
+      "startLine": 3814
     },
     {
       "endCharacter": 24,
-      "endLine": 3291,
+      "endLine": 3835,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"X_test\" of type \"DataFrame\" in function \"make_test_set_and_weights\"\n  Type \"Any | None\" is not assignable to type \"DataFrame\"\n    \"None\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 3291
+      "startLine": 3835
     },
     {
       "endCharacter": 24,
-      "endLine": 3292,
+      "endLine": 3836,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"y_test\" of type \"DataFrame\" in function \"make_test_set_and_weights\"\n  Type \"Any | None\" is not assignable to type \"DataFrame\"\n    \"None\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 3292
+      "startLine": 3836
     },
     {
       "endCharacter": 30,
-      "endLine": 3293,
+      "endLine": 3837,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"test_weights\" of type \"NDArray[floating[Any]]\" in function \"make_test_set_and_weights\"\n  Type \"Any | None\" is not assignable to type \"NDArray[floating[Any]]\"\n    \"None\" is not assignable to \"ndarray[_AnyShape, dtype[floating[Any]]]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 3293
+      "startLine": 3837
     },
     {
       "endCharacter": 15,
-      "endLine": 3299,
+      "endLine": 3843,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"X\" of type \"DataFrame\" in function \"fit_regressor\"\n  Type \"Any | None\" is not assignable to type \"DataFrame\"\n    \"None\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 14,
-      "startLine": 3299
+      "startLine": 3843
     },
     {
       "endCharacter": 15,
-      "endLine": 3300,
+      "endLine": 3844,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"y\" of type \"DataFrame\" in function \"fit_regressor\"\n  Type \"Any | None\" is not assignable to type \"DataFrame\"\n    \"None\" is not assignable to \"DataFrame\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 14,
-      "startLine": 3300
+      "startLine": 3844
     },
     {
       "endCharacter": 39,
-      "endLine": 3301,
+      "endLine": 3845,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"train_weights\" of type \"NDArray[floating[Any]]\" in function \"fit_regressor\"\n  Type \"Any | None\" is not assignable to type \"NDArray[floating[Any]]\"\n    \"None\" is not assignable to \"ndarray[_AnyShape, dtype[floating[Any]]]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 26,
-      "startLine": 3301
+      "startLine": 3845
     },
     {
       "endCharacter": 38,
-      "endLine": 3311,
+      "endLine": 3855,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "\"columns\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 31,
-      "startLine": 3311
+      "startLine": 3855
     },
     {
       "endCharacter": 34,
-      "endLine": 3312,
+      "endLine": 3856,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "\"index\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 29,
-      "startLine": 3312
+      "startLine": 3856
     },
     {
       "endCharacter": 82,
-      "endLine": 3314,
+      "endLine": 3858,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "\"copy\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 78,
-      "startLine": 3314
+      "startLine": 3858
     },
     {
       "endCharacter": 62,
-      "endLine": 3757,
+      "endLine": 4278,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "\"get_pair_dataframe\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 44,
-      "startLine": 3757
+      "startLine": 4278
     },
     {
       "endCharacter": 68,
-      "endLine": 4137,
+      "endLine": 4665,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Type \"floating[Any] | NDArray[float64]\" is not assignable to return type \"NDArray[floating[Any]]\"\n  Type \"floating[Any] | NDArray[float64]\" is not assignable to type \"NDArray[floating[Any]]\"\n    \"floating[Any]\" is not assignable to \"ndarray[_AnyShape, dtype[floating[Any]]]\"",
       "rule": "reportReturnType",
       "severity": "error",
       "startCharacter": 15,
-      "startLine": 4133
+      "startLine": 4661
     },
     {
       "endCharacter": 13,
-      "endLine": 4166,
+      "endLine": 4694,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "No overloads for \"cdist\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 19,
-      "startLine": 4161
+      "startLine": 4689
     },
     {
       "endCharacter": 38,
-      "endLine": 4164,
+      "endLine": 4692,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"str\" cannot be assigned to parameter \"metric\" of type \"_MetricFunc\" in function \"cdist\"\n  Type \"str\" is not assignable to type \"_MetricFunc\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 23,
-      "startLine": 4164
+      "startLine": 4692
     },
     {
       "endCharacter": 9,
-      "endLine": 4269,
+      "endLine": 4797,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "No overloads for \"pdist\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 36,
-      "startLine": 4267
+      "startLine": 4795
     },
     {
       "endCharacter": 42,
-      "endLine": 4268,
+      "endLine": 4796,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"str\" cannot be assigned to parameter \"metric\" of type \"_MetricFunc\" in function \"pdist\"\n  Type \"str\" is not assignable to type \"_MetricFunc\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 27,
-      "startLine": 4268
+      "startLine": 4796
     },
     {
       "endCharacter": 9,
-      "endLine": 4346,
+      "endLine": 4874,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "No overloads for \"cdist\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 15,
-      "startLine": 4341
+      "startLine": 4869
     },
     {
       "endCharacter": 34,
-      "endLine": 4344,
+      "endLine": 4872,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"str\" cannot be assigned to parameter \"metric\" of type \"_MetricFunc\" in function \"cdist\"\n  Type \"str\" is not assignable to type \"_MetricFunc\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 19,
-      "startLine": 4344
+      "startLine": 4872
     },
     {
       "endCharacter": 99,
-      "endLine": 4535,
+      "endLine": 5063,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "No overloads for \"pdist\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 4535
+      "startLine": 5063
     },
     {
       "endCharacter": 79,
-      "endLine": 4535,
+      "endLine": 5063,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "Argument of type \"str\" cannot be assigned to parameter \"metric\" of type \"_MetricFunc\" in function \"pdist\"\n  Type \"str\" is not assignable to type \"_MetricFunc\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 64,
-      "startLine": 4535
+      "startLine": 5063
     },
     {
       "endCharacter": 55,
     },
     {
       "endCharacter": 19,
-      "endLine": 331,
+      "endLine": 327,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "\"protections\" overrides symbol of same name in class \"IStrategy\"\n  \"cached_property[list[dict[str, Any]]]\" is not assignable to \"list[Unknown]\"",
       "rule": "reportIncompatibleVariableOverride",
       "severity": "error",
       "startCharacter": 8,
-      "startLine": 331
+      "startLine": 327
     },
     {
       "endCharacter": 28,
-      "endLine": 404,
+      "endLine": 400,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "\"startup_candle_count\" overrides symbol of same name in class \"IStrategy\"\n  \"property\" is not assignable to \"int\"",
       "rule": "reportIncompatibleVariableOverride",
       "severity": "error",
       "startCharacter": 8,
-      "startLine": 404
+      "startLine": 400
     },
     {
       "endCharacter": 44,
-      "endLine": 513,
+      "endLine": 509,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"/\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 513
+      "startLine": 509
     },
     {
       "endCharacter": 38,
-      "endLine": 787,
+      "endLine": 783,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "\"to_numpy\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 30,
-      "startLine": 787
+      "startLine": 783
     },
     {
       "endCharacter": 44,
-      "endLine": 820,
+      "endLine": 816,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"-\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 38,
-      "startLine": 820
+      "startLine": 816
     },
     {
       "endCharacter": 38,
-      "endLine": 828,
+      "endLine": 824,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Object of type \"None\" is not subscriptable",
       "rule": "reportOptionalSubscript",
       "severity": "error",
       "startCharacter": 36,
-      "startLine": 828
+      "startLine": 824
     },
     {
       "endCharacter": 39,
-      "endLine": 829,
+      "endLine": 825,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Object of type \"None\" is not subscriptable",
       "rule": "reportOptionalSubscript",
       "severity": "error",
       "startCharacter": 37,
-      "startLine": 829
+      "startLine": 825
     },
     {
       "endCharacter": 38,
-      "endLine": 830,
+      "endLine": 826,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Object of type \"None\" is not subscriptable",
       "rule": "reportOptionalSubscript",
       "severity": "error",
       "startCharacter": 36,
-      "startLine": 830
+      "startLine": 826
     },
     {
       "endCharacter": 44,
-      "endLine": 853,
+      "endLine": 849,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"-\" not supported for types \"Series[Any] | None\" and \"Series[Any] | None\"\n  Operator \"-\" not supported for types \"Series[Any]\" and \"None\"\n  Operator \"-\" not supported for types \"None\" and \"Series[Any]\"\n  Operator \"-\" not supported for types \"None\" and \"None\"",
       "rule": "reportOperatorIssue",
       "severity": "error",
       "startCharacter": 29,
-      "startLine": 853
+      "startLine": 849
     },
     {
       "endCharacter": 66,
-      "endLine": 853,
+      "endLine": 849,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"s1\" of type \"Series[Any]\" in function \"non_zero_diff\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 61,
-      "startLine": 853
+      "startLine": 849
     },
     {
       "endCharacter": 72,
-      "endLine": 853,
+      "endLine": 849,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"s2\" of type \"Series[Any]\" in function \"non_zero_diff\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 68,
-      "startLine": 853
+      "startLine": 849
     },
     {
       "endCharacter": 56,
-      "endLine": 859,
+      "endLine": 855,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"p1\" of type \"T@get_distance\" in function \"get_distance\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 50,
-      "startLine": 859
+      "startLine": 855
     },
     {
       "endCharacter": 58,
-      "endLine": 860,
+      "endLine": 856,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"p1\" of type \"T@get_distance\" in function \"get_distance\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 52,
-      "startLine": 860
+      "startLine": 856
     },
     {
       "endCharacter": 57,
-      "endLine": 861,
+      "endLine": 857,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"p1\" of type \"T@get_distance\" in function \"get_distance\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 51,
-      "startLine": 861
+      "startLine": 857
     },
     {
       "endCharacter": 44,
-      "endLine": 864,
+      "endLine": 860,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"series\" of type \"Series[Any]\" in function \"zlema\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 38,
-      "startLine": 864
+      "startLine": 860
     },
     {
       "endCharacter": 44,
-      "endLine": 865,
+      "endLine": 861,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"series\" of type \"Series[Any]\" in function \"zlema\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 38,
-      "startLine": 865
+      "startLine": 861
     },
     {
       "endCharacter": 44,
-      "endLine": 866,
+      "endLine": 862,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"series\" of type \"Series[Any]\" in function \"zlema\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 38,
-      "startLine": 866
+      "startLine": 862
     },
     {
       "endCharacter": 61,
-      "endLine": 867,
+      "endLine": 863,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"p1\" of type \"T@get_distance\" in function \"get_distance\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 55,
-      "startLine": 867
+      "startLine": 863
     },
     {
       "endCharacter": 61,
-      "endLine": 868,
+      "endLine": 864,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"p1\" of type \"T@get_distance\" in function \"get_distance\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 55,
-      "startLine": 868
+      "startLine": 864
     },
     {
       "endCharacter": 61,
-      "endLine": 869,
+      "endLine": 865,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"p1\" of type \"T@get_distance\" in function \"get_distance\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 55,
-      "startLine": 869
+      "startLine": 865
     },
     {
       "endCharacter": 81,
-      "endLine": 877,
+      "endLine": 873,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any]\" cannot be assigned to parameter \"p2\" of type \"T@get_distance\" in function \"get_distance\"\n  \"Series[Any]\" is not assignable to \"float\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 58,
-      "startLine": 877
+      "startLine": 873
     },
     {
       "endCharacter": 67,
-      "endLine": 890,
+      "endLine": 886,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"p1\" of type \"T@get_distance\" in function \"get_distance\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 61,
-      "startLine": 890
+      "startLine": 886
     },
     {
       "endCharacter": 68,
-      "endLine": 891,
+      "endLine": 887,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"p1\" of type \"T@get_distance\" in function \"get_distance\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 62,
-      "startLine": 891
+      "startLine": 887
     },
     {
       "endCharacter": 67,
-      "endLine": 892,
+      "endLine": 888,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"p1\" of type \"T@get_distance\" in function \"get_distance\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 61,
-      "startLine": 892
+      "startLine": 888
     },
     {
       "endCharacter": 44,
-      "endLine": 893,
+      "endLine": 889,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"-\" not supported for types \"Series[Any] | None\" and \"Series[Any] | None\"\n  Operator \"-\" not supported for types \"Series[Any]\" and \"None\" when expected type is \"_SetItemValueNotDataFrame\"\n  Operator \"-\" not supported for types \"None\" and \"Series[Any]\" when expected type is \"_SetItemValueNotDataFrame\"\n  Operator \"-\" not supported for types \"None\" and \"None\" when expected type is \"_SetItemValueNotDataFrame\"",
       "rule": "reportOperatorIssue",
       "severity": "error",
       "startCharacter": 30,
-      "startLine": 893
+      "startLine": 889
     },
     {
       "endCharacter": 56,
-      "endLine": 894,
+      "endLine": 890,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "No overloads for \"__call__\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 31,
-      "startLine": 894
+      "startLine": 890
     },
     {
       "endCharacter": 47,
-      "endLine": 894,
+      "endLine": 890,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"x1\" of type \"ArrayLike\" in function \"__call__\"\n  Type \"Series[Any] | None\" is not assignable to type \"ArrayLike\"\n    Type \"None\" is not assignable to type \"ArrayLike\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"_SupportsArray[dtype[Any]]\"\n        \"__array__\" is not present\n      \"None\" is incompatible with protocol \"_NestedSequence[_SupportsArray[dtype[Any]]]\"\n        \"__len__\" is not present\n  ...",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 42,
-      "startLine": 894
+      "startLine": 890
     },
     {
       "endCharacter": 55,
-      "endLine": 894,
+      "endLine": 890,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"x2\" of type \"ArrayLike\" in function \"__call__\"\n  Type \"Series[Any] | None\" is not assignable to type \"ArrayLike\"\n    Type \"None\" is not assignable to type \"ArrayLike\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"_SupportsArray[dtype[Any]]\"\n        \"__array__\" is not present\n      \"None\" is incompatible with protocol \"_NestedSequence[_SupportsArray[dtype[Any]]]\"\n        \"__len__\" is not present\n  ...",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 49,
-      "startLine": 894
+      "startLine": 890
     },
     {
       "endCharacter": 64,
-      "endLine": 895,
+      "endLine": 891,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "No overloads for \"__call__\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 39,
-      "startLine": 895
+      "startLine": 891
     },
     {
       "endCharacter": 55,
-      "endLine": 895,
+      "endLine": 891,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"x1\" of type \"ArrayLike\" in function \"__call__\"\n  Type \"Series[Any] | None\" is not assignable to type \"ArrayLike\"\n    Type \"None\" is not assignable to type \"ArrayLike\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"_SupportsArray[dtype[Any]]\"\n        \"__array__\" is not present\n      \"None\" is incompatible with protocol \"_NestedSequence[_SupportsArray[dtype[Any]]]\"\n        \"__len__\" is not present\n  ...",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 50,
-      "startLine": 895
+      "startLine": 891
     },
     {
       "endCharacter": 63,
-      "endLine": 895,
+      "endLine": 891,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"x2\" of type \"ArrayLike\" in function \"__call__\"\n  Type \"Series[Any] | None\" is not assignable to type \"ArrayLike\"\n    Type \"None\" is not assignable to type \"ArrayLike\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"_SupportsArray[dtype[Any]]\"\n        \"__array__\" is not present\n      \"None\" is incompatible with protocol \"_NestedSequence[_SupportsArray[dtype[Any]]]\"\n        \"__len__\" is not present\n  ...",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 57,
-      "startLine": 895
+      "startLine": 891
     },
     {
       "endCharacter": 55,
-      "endLine": 903,
+      "endLine": 899,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"p1\" of type \"T@get_distance\" in function \"get_distance\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 49,
-      "startLine": 903
+      "startLine": 899
     },
     {
       "endCharacter": 55,
-      "endLine": 904,
+      "endLine": 900,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"p1\" of type \"T@get_distance\" in function \"get_distance\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 49,
-      "startLine": 904
+      "startLine": 900
     },
     {
       "endCharacter": 55,
-      "endLine": 905,
+      "endLine": 901,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"p1\" of type \"T@get_distance\" in function \"get_distance\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 49,
-      "startLine": 905
+      "startLine": 901
     },
     {
       "endCharacter": 55,
-      "endLine": 906,
+      "endLine": 902,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"p1\" of type \"T@get_distance\" in function \"get_distance\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 49,
-      "startLine": 906
+      "startLine": 902
     },
     {
       "endCharacter": 55,
-      "endLine": 907,
+      "endLine": 903,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"p1\" of type \"T@get_distance\" in function \"get_distance\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 49,
-      "startLine": 907
+      "startLine": 903
     },
     {
       "endCharacter": 55,
-      "endLine": 908,
+      "endLine": 904,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"p1\" of type \"T@get_distance\" in function \"get_distance\"\n  Type \"Series[Any] | None\" is not assignable to type \"Series[Any]\"\n    \"None\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 49,
-      "startLine": 908
+      "startLine": 904
     },
     {
       "endCharacter": 50,
-      "endLine": 937,
+      "endLine": 933,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.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": 44,
-      "startLine": 937
+      "startLine": 933
     },
     {
       "endCharacter": 50,
-      "endLine": 937,
+      "endLine": 933,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.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": 44,
-      "startLine": 937
+      "startLine": 933
     },
     {
       "endCharacter": 29,
-      "endLine": 938,
+      "endLine": 934,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.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": 23,
-      "startLine": 938
+      "startLine": 934
     },
     {
       "endCharacter": 29,
-      "endLine": 938,
+      "endLine": 934,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.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": 23,
-      "startLine": 938
+      "startLine": 934
     },
     {
       "endCharacter": 60,
-      "endLine": 975,
+      "endLine": 971,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.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": 50,
-      "startLine": 975
+      "startLine": 971
     },
     {
       "endCharacter": 60,
-      "endLine": 975,
+      "endLine": 971,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.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": 50,
-      "startLine": 975
+      "startLine": 971
     },
     {
       "endCharacter": 35,
-      "endLine": 976,
+      "endLine": 972,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.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": 25,
-      "startLine": 976
+      "startLine": 972
     },
     {
       "endCharacter": 35,
-      "endLine": 976,
+      "endLine": 972,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.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": 25,
-      "startLine": 976
+      "startLine": 972
     },
     {
       "endCharacter": 41,
-      "endLine": 1201,
+      "endLine": 1197,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"<\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 1201
+      "startLine": 1197
     },
     {
       "endCharacter": 41,
-      "endLine": 1211,
+      "endLine": 1207,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \">\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 1211
+      "startLine": 1207
     },
     {
       "endCharacter": 66,
-      "endLine": 1224,
+      "endLine": 1220,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"timeframe\" of type \"str\" in function \"timeframe_to_prev_date\"\n  Type \"Any | None\" is not assignable to type \"str\"\n    \"None\" is not assignable to \"str\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 38,
-      "startLine": 1224
+      "startLine": 1220
     },
     {
       "endCharacter": 70,
-      "endLine": 1355,
+      "endLine": 1351,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Expected 1 more positional argument",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 48,
-      "startLine": 1355
+      "startLine": 1351
     },
     {
       "endCharacter": 74,
-      "endLine": 1419,
+      "endLine": 1415,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"int | None\" cannot be assigned to parameter \"trade_duration_candles\" of type \"int\" in function \"get_trade_natr\"\n  Type \"int | None\" is not assignable to type \"int\"\n    \"None\" is not assignable to \"int\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 52,
-      "startLine": 1419
+      "startLine": 1415
     },
     {
       "endCharacter": 74,
-      "endLine": 1446,
+      "endLine": 1442,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"int | None\" cannot be assigned to parameter \"trade_duration_candles\" of type \"int\" in function \"get_trade_natr\"\n  Type \"int | None\" is not assignable to type \"int\"\n    \"None\" is not assignable to \"int\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 52,
-      "startLine": 1446
+      "startLine": 1442
     },
     {
       "endCharacter": 96,
-      "endLine": 1498,
+      "endLine": 1494,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"timeframe\" of type \"str\" in function \"get_analyzed_dataframe\"\n  Type \"Any | None\" is not assignable to type \"str\"\n    \"None\" is not assignable to \"str\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 68,
-      "startLine": 1498
+      "startLine": 1494
     },
     {
       "endCharacter": 77,
-      "endLine": 1528,
+      "endLine": 1524,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"<=\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 57,
-      "startLine": 1528
+      "startLine": 1524
     },
     {
       "endCharacter": 9,
-      "endLine": 1533,
+      "endLine": 1529,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"+\" not supported for types \"float\" and \"float | None\"\n  Operator \"+\" not supported for types \"float\" and \"None\"",
       "rule": "reportOperatorIssue",
       "severity": "error",
       "startCharacter": 28,
-      "startLine": 1531
+      "startLine": 1527
     },
     {
       "endCharacter": 33,
-      "endLine": 1532,
+      "endLine": 1528,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"-\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 13,
-      "startLine": 1532
+      "startLine": 1528
     },
     {
       "endCharacter": 67,
-      "endLine": 1538,
+      "endLine": 1534,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.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": 47,
-      "startLine": 1538
+      "startLine": 1534
     },
     {
       "endCharacter": 67,
-      "endLine": 1538,
+      "endLine": 1534,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.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": 47,
-      "startLine": 1538
+      "startLine": 1534
     },
     {
       "endCharacter": 64,
-      "endLine": 1692,
+      "endLine": 1688,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "\"isoformat\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 55,
-      "startLine": 1692
+      "startLine": 1688
     },
     {
       "endCharacter": 37,
-      "endLine": 1743,
+      "endLine": 1739,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"<=\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 15,
-      "startLine": 1743
+      "startLine": 1739
     },
     {
       "endCharacter": 48,
-      "endLine": 1745,
+      "endLine": 1741,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"<=\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 15,
-      "startLine": 1745
+      "startLine": 1741
     },
     {
       "endCharacter": 96,
-      "endLine": 1925,
+      "endLine": 1921,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"timeframe\" of type \"str\" in function \"get_analyzed_dataframe\"\n  Type \"Any | None\" is not assignable to type \"str\"\n    \"None\" is not assignable to \"str\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 68,
-      "startLine": 1925
+      "startLine": 1921
     },
     {
       "endCharacter": 39,
-      "endLine": 2028,
+      "endLine": 2024,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"+\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 21,
-      "startLine": 2028
+      "startLine": 2024
     },
     {
       "endCharacter": 90,
-      "endLine": 2028,
+      "endLine": 2024,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"*\" not supported for types \"float\" and \"Any | None\"\n  Operator \"*\" not supported for types \"float\" and \"None\"",
       "rule": "reportOperatorIssue",
       "severity": "error",
       "startCharacter": 62,
-      "startLine": 2028
+      "startLine": 2024
     },
     {
       "endCharacter": 96,
-      "endLine": 2332,
+      "endLine": 2328,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"timeframe\" of type \"str\" in function \"get_analyzed_dataframe\"\n  Type \"Any | None\" is not assignable to type \"str\"\n    \"None\" is not assignable to \"str\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 68,
-      "startLine": 2332
+      "startLine": 2328
     },
     {
       "endCharacter": 85,
-      "endLine": 2357,
+      "endLine": 2353,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "\"isoformat\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 76,
-      "startLine": 2357
+      "startLine": 2353
     },
     {
       "endCharacter": 47,
-      "endLine": 2363,
+      "endLine": 2359,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"<\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 2363
+      "startLine": 2359
     },
     {
       "endCharacter": 42,
-      "endLine": 2367,
+      "endLine": 2363,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"str\" cannot be assigned to parameter \"side\" of type \"TradeDirection\" in function \"reversal_confirmed\"\n  Type \"str\" is not assignable to type \"TradeDirection\"\n    \"str\" is not assignable to type \"Literal['long']\"\n    \"str\" is not assignable to type \"Literal['short']\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 2367
+      "startLine": 2363
     },
     {
       "endCharacter": 42,
-      "endLine": 2368,
+      "endLine": 2364,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"str\" cannot be assigned to parameter \"order\" of type \"OrderType\" in function \"reversal_confirmed\"\n  Type \"str\" is not assignable to type \"OrderType\"\n    \"str\" is not assignable to type \"Literal['entry']\"\n    \"str\" is not assignable to type \"Literal['exit']\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 2368
+      "startLine": 2364
     },
     {
       "endCharacter": 69,
-      "endLine": 2370,
+      "endLine": 2366,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"int | float\" cannot be assigned to parameter \"lookback_period_candles\" of type \"int\" in function \"reversal_confirmed\"\n  Type \"int | float\" is not assignable to type \"int\"\n    \"float\" is not assignable to \"int\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 2370
+      "startLine": 2366
     },
     {
       "endCharacter": 47,
-      "endLine": 2381,
+      "endLine": 2377,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \">\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 2381
+      "startLine": 2377
     },
     {
       "endCharacter": 43,
-      "endLine": 2385,
+      "endLine": 2381,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"str\" cannot be assigned to parameter \"side\" of type \"TradeDirection\" in function \"reversal_confirmed\"\n  Type \"str\" is not assignable to type \"TradeDirection\"\n    \"str\" is not assignable to type \"Literal['long']\"\n    \"str\" is not assignable to type \"Literal['short']\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 2385
+      "startLine": 2381
     },
     {
       "endCharacter": 42,
-      "endLine": 2386,
+      "endLine": 2382,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"str\" cannot be assigned to parameter \"order\" of type \"OrderType\" in function \"reversal_confirmed\"\n  Type \"str\" is not assignable to type \"OrderType\"\n    \"str\" is not assignable to type \"Literal['entry']\"\n    \"str\" is not assignable to type \"Literal['exit']\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 2386
+      "startLine": 2382
     },
     {
       "endCharacter": 69,
-      "endLine": 2388,
+      "endLine": 2384,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"int | float\" cannot be assigned to parameter \"lookback_period_candles\" of type \"int\" in function \"reversal_confirmed\"\n  Type \"int | float\" is not assignable to type \"int\"\n    \"float\" is not assignable to \"int\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 2388
+      "startLine": 2384
     },
     {
       "endCharacter": 52,
-      "endLine": 2417,
+      "endLine": 2413,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"datetime | None\" cannot be assigned to parameter \"current_candle_date\" of type \"datetime\" in function \"_normalize_final_take_profit_state\"\n  Type \"datetime | None\" is not assignable to type \"datetime\"\n    \"None\" is not assignable to \"datetime\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 36,
-      "startLine": 2417
+      "startLine": 2413
     },
     {
       "endCharacter": 96,
-      "endLine": 2537,
+      "endLine": 2533,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"timeframe\" of type \"str\" in function \"get_analyzed_dataframe\"\n  Type \"Any | None\" is not assignable to type \"str\"\n    \"None\" is not assignable to \"str\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 68,
-      "startLine": 2537
+      "startLine": 2533
     },
     {
       "endCharacter": 43,
-      "endLine": 2546,
+      "endLine": 2542,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"str\" cannot be assigned to parameter \"order\" of type \"OrderType\" in function \"reversal_confirmed\"\n  Type \"str\" is not assignable to type \"OrderType\"\n    \"str\" is not assignable to type \"Literal['entry']\"\n    \"str\" is not assignable to type \"Literal['exit']\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 2546
+      "startLine": 2542
     },
     {
       "endCharacter": 69,
-      "endLine": 2548,
+      "endLine": 2544,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"int | float\" cannot be assigned to parameter \"lookback_period_candles\" of type \"int\" in function \"reversal_confirmed\"\n  Type \"int | float\" is not assignable to type \"int\"\n    \"float\" is not assignable to \"int\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 2548
+      "startLine": 2544
     },
     {
       "endCharacter": 32,
index 51776fab2e7871243e37197512e7283a7d9930c0..7d95a4b6bde0b5b64300702ed5aded1908f73b66 100644 (file)
@@ -11,7 +11,9 @@ morphology, not returns. Regressor error is a model diagnostic, not a profit sco
 - [Models and HPO](#models-and-hpo)
 - [Configuration formats](#configuration-formats)
 - [Continual learning](#continual-learning)
+- [Backtest cache reuse](#backtest-cache-reuse)
 - [Prediction warmup and restarts](#prediction-warmup-and-restarts)
+- [Prediction diagnostics and gaps](#prediction-diagnostics-and-gaps)
 - [Configuration reference](#configuration-reference)
 
 ## Quick start
@@ -116,12 +118,12 @@ Use model-specific training parameters supported by the selected backend.
 
 `freqai.optuna_hyperopt.enabled=false` disables regressor and dynamic label
 HPO. When enabled, trial, worker and timeout budgets apply. Label HPO selects
-morphology parameters, not held-out trading profit. `holdout_rmse` reports
-weighted error on the original label scale before deployment refit; unavailable
-holdouts produce infinity. Use the [evaluation protocol](../docs/evaluation.md)
-for economic comparison and the limitations of threshold replay in native
-backtests. Changing fitted pipelines, label weighting or study schemas can
-require a new `freqai.identifier` or an intentional reset; see the reference.
+morphology parameters, not held-out trading profit. See
+[Prediction diagnostics and gaps](#prediction-diagnostics-and-gaps) for model
+error and graph interpretation. Use the [evaluation protocol](../docs/evaluation.md)
+for economic comparisons and the limits of native backtests. Changing fitted
+pipelines, label weighting or study schemas can require a new `freqai.identifier`
+or an intentional reset; see the reference.
 
 Live and dry-run initialization restores accepted per-pair label parameters
 (`label_period_candles` and `label_natr_multiplier`) saved under
@@ -147,34 +149,119 @@ later model left under the same identifier is not reused when a backtest is
 extended into the past. Live and dry-run restarts still restore compatible
 deployed models.
 
+## Backtest cache reuse
+
+Backtests store reusable models and predictions under
+`models/<identifier>/backtesting/<fingerprint>/`. Cache identity includes input
+data, effective model and strategy configuration (including settings outside
+`freqai`), effective label and native strategy parameters, strategy/model code
+and dependency versions.
+Changing these inputs or extending the dataset uses a separate cache directory.
+Even configuration changes unrelated to training can therefore trigger retraining.
+Existing artifacts remain on disk; they are not deleted.
+
+Native strategy parameters are compared after defaults and the strategy's
+adjacent JSON parameter file have been applied. Editing that file does not
+invalidate the cache if effective parameter values and all other inputs stay
+unchanged.
+
+Any prepared input column with pandas `object` or `category` dtype disables
+reuse, including non-feature inputs and structured orderflow data. Value hashing
+does not preserve scalar types or category metadata. Each run therefore uses a
+fresh directory, increasing training time and disk use. Plain text can use pandas
+`string` dtype to retain reuse, provided conversion preserves target semantics.
+
+Cached and fresh windows return the same predictions and prepared indicator
+columns. `freqai_backtest_live_models=true` is a separate Freqtrade mode: it
+replays stored live predictions without training models.
+
 ## Prediction warmup and restarts
 
-In live and dry-run modes, each pair requires
-`freqai.fit_live_predictions_candles` real model predictions before adaptive
-thresholds become available. The Nth observation first affects the next
-prediction update, not the candle that produced it. FreqAI bootstrap
-predictions made from the initial training frame do not count. Warmup progress
-from persisted real predictions is restored after a restart. FreqAI saves
-prediction history after training attempts and on clean shutdown. After an
-abrupt stop or hard reboot, predictions since the last history save may be
-lost, so those observations must accumulate again. Legacy rows missing
-provenance, including rows in partly marked histories, count only when their
-nonzero, nonexpired prediction status distinguishes them from bootstrap;
-ambiguous rejected rows and explicit false markers do not count.
-
-A pair starts a new warmup when the time since its last observation, or a gap
-within its observations, is greater than
+### When thresholds become available
+
+Each pair needs `freqai.fit_live_predictions_candles` real model predictions
+before adaptive thresholds become available. The Nth observation first affects
+the next prediction update, not the candle that produced it. Genuine zero and
+outlier-rejected predictions count; FreqAI bootstrap predictions, missing outputs
+and expired-model rows do not.
+
+For older histories without reliable provenance, only distinguishable model
+outputs count. Ambiguous rows remain excluded, even when their displayed value
+is zero; see [`prediction_available`](#prediction-diagnostics-and-gaps).
+
+### Missing observations
+
+Missing predictions delay warmup. A pair starts a new warmup if a gap between
+eligible observations, or the time since its last observation, exceeds
 `fit_live_predictions_candles × timeframe`. An observation exactly one horizon
-old remains eligible. Downtime and expired-model rows are excluded; genuine
-zero and outlier-rejected predictions count. Predictions align to candle dates;
-candles without predictions have `do_predict=0` and downtime zeros display but
-never calibrate.
+old remains eligible.
+
+### After a restart
+
+Persisted real predictions restore warmup progress. FreqAI saves prediction
+history after training attempts and on clean shutdown. An abrupt stop or reboot
+can lose unsaved observations, so they must accumulate again.
+
+## Prediction diagnostics and gaps
+
+### Prediction availability
 
-After a restart, persisted `holdout_rmse` is recovered from the latest produced
-prediction available at the decision time, independently of the calibration
-window. A newer unavailable score remains unavailable rather than reviving an
-older finite score. Recovery does not warm up adaptive thresholds; a model fitted
-in the current session keeps its in-memory score.
+`prediction_available` describes whether a model output is available, not whether
+an entry is allowed. The [decision cycle](#decision-cycle) defines the entry and
+exit conditions.
+
+| Value               | Meaning                                                                                                                              |
+| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
+| `1`                 | A finite model output from complete inputs, including genuine zero and outlier-rejected outputs.                                     |
+| `0`                 | No usable output: no model, bootstrap, missing inputs or history, non-finite output, downtime, model expiration or backtest padding. |
+| `NaN` / JSON `null` | Unknown provenance in an older history. Existing values are retained but do not count toward calibration.                            |
+
+Stored-live-prediction replay uses the same availability values: a candle without
+a matching history row has value `0`; an existing ambiguous legacy row remains
+unknown.
+
+Missing or infinite input features remain rejected after preprocessing;
+replacing missing inputs with zero does not make them valid. Non-finite model
+outputs are also rejected and excluded from warmup.
+
+For known absences, predictions, their statistics and thresholds, RMSE and DI
+diagnostics appear as graph gaps (`NaN`) and JSON `null`. DI diagnostics include
+the values, fitted distribution parameters and cutoff. Genuine zeros remain
+visible. Already persisted zeros cannot be identified retrospectively as earlier
+missing values.
+
+### Model error and recovery
+
+`holdout_rmse` measures weighted prediction error on the original label scale,
+using the holdout before deployment refit. It is not a trading-profit metric.
+Unknown, negative or non-finite scores appear as gaps, not as a perfect score
+of zero. A genuine zero RMSE remains visible.
+
+After a restart, the loaded model's score takes precedence. Only when its
+`holdout_rmse` field is absent does recovery use the score recorded with the
+latest real prediction available at the decision time, independently of the
+calibration window. A newer unavailable score does not revive an older finite one.
+A model trained in the current run keeps its own score.
+
+Restoring RMSE does not make adaptive thresholds ready; prediction warmup still
+applies. New output columns remain missing on older candles instead of
+backfilling them with current values.
+
+### Historical target plots
+
+Before FreqAI, `&s-extrema` contains the training target; after FreqAI, it contains
+the model prediction. The historical target direction and label-weight subplots
+show target construction, not forecasts.
+Raw direction and label weights use bars; their smoothed values use curves.
+
+Raw Zigzag direction is sparse (-1/0/+1). Smoothed direction can be zero outside
+the smoothing support or through cancellation. A flat series with no confirmed
+pivot does not provide usable labels merely because its target values are zero.
+
+The `extrema_direction`, `extrema_direction_smoothed`, `extrema_weight` and
+`extrema_weight_smoothed` diagnostics also appear in backtests. They are not model
+features. Missing diagnostics, including weights when weighting is disabled,
+remain `NaN`, not synthesized zeros.
 
 ## Configuration reference
 
@@ -216,6 +303,11 @@ and the [FreqAI parameter reference][freqai-parameters].
 | -------- | ------------------- | ------------------------- | ------------------------------------------------------- |
 | leverage | `proposed_leverage` | float [1.0, max_leverage] | Leverage. Fallback to `proposed_leverage` for the pair. |
 
+Configured leverage must be numeric, finite and representable as a float.
+Strings, booleans, nonfinite values and conversion overflow log a warning and use
+the callback's `proposed_leverage`. Missing or null values also use that proposal.
+The effective leverage is clamped to `[1.0, max_leverage]`, including proposal fallbacks.
+
 ### Exit pricing
 
 | Path                                                           | Runtime fallback       | Type / Range                                                        | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
index 07c37e7c3a139911f9de328a56fcdf5570c53ce7..99ef9c23309aac1846f1346d7aced290468384b5 100644 (file)
@@ -66,8 +66,8 @@ def _merge(base: dict[str, Any], overrides: dict[str, Any]) -> dict[str, Any]:
 def model_config(tmp_path: Path | str, **overrides: Any) -> dict[str, Any]:
     """Return a private Freqtrade config rooted at `tmp_path`, deep-merged with `overrides`."""
     config = _merge(_BASE_CONFIG, overrides)
-    config["user_data_dir"] = tmp_path
-    config["runmode"] = RunMode.DRY_RUN
+    config["user_data_dir"] = Path(config.get("user_data_dir", tmp_path))
+    config.setdefault("runmode", RunMode.DRY_RUN)
     return config
 
 
index 1ae523dd49bc5d4388ae69f1d8a7f9339558041d..e0c32aa4e226f135c16b37a578c92d0d782b8fa2 100644 (file)
@@ -1,12 +1,18 @@
 """Guards the coverage gate itself; requires the Freqtrade QA image."""
 
+import ast
 import configparser
 import os
 import re
 import subprocess
 import unittest
 from pathlib import Path
+from unittest import mock
 
+from coverage.config import DEFAULT_EXCLUDE, DEFAULT_PARTIAL, DEFAULT_PARTIAL_ALWAYS, CoverageConfig
+from coverage.files import find_python_files
+from coverage.misc import join_regex
+from coverage.parser import PythonParser
 from qa_support import COVERAGERC, REPO_ROOT, QaTestCase, temporary_directory
 
 FLOOR = "fail_under"
@@ -16,14 +22,34 @@ NAMESPACE_PACKAGES = "include_namespace_packages"
 OMIT = "omit"
 # The lowest floor the project will accept. Raising it is free; dropping below this is the
 # silent gate disabling that `.coveragerc` and README.md:532 both forbid.
-MINIMUM_FLOOR = 67.0
+MINIMUM_FLOOR = 68.0
 PRAGMA = re.compile(r"#\s*pragma\s*:?\s*no\s*(?:cover|branch)\b", re.IGNORECASE)
-# The other two of coverage.py's three DEFAULT_EXCLUDE patterns.
-ELLIPSIS_BODY = re.compile(r"^\s*(((async )?def .*?)?[\])]+(\s*->.*?)?:\s*)?\.\.\.\s*(#|$)")
-TYPE_CHECKING = re.compile(r"^\s*if (typing\.)?TYPE_CHECKING:")
 INCLUDE = "include"
 MEASURED_TREE = REPO_ROOT / "quickadapter" / "user_data"
 COVERAGE_RUNNER = REPO_ROOT / "scripts" / "run-coverage.sh"
+EXCLUSION_KEYS = (
+    "omit",
+    "include",
+    "patch",
+    "exclude_lines",
+    "exclude_also",
+    "partial_branches",
+    "partial_also",
+    "partial_branches_always",
+)
+
+
+def _branch_counts(parser: PythonParser) -> dict[int, int]:
+    """Count the branch units that coverage actually includes in its denominator."""
+    return {line: count for line, count in parser.exit_counts().items() if count > 1}
+
+
+def _legal_options(section: str) -> set[str]:
+    return {
+        spec[1].split(":")[1]
+        for spec in CoverageConfig.CONFIG_FILE_OPTIONS
+        if spec[1].startswith(f"{section}:") and spec[1] != "run:_crash"
+    }
 
 
 class CoverageFloorTest(QaTestCase):
@@ -33,6 +59,18 @@ class CoverageFloorTest(QaTestCase):
         self.parser = configparser.ConfigParser()
         self.parser.read_string(self.text)
 
+    def test_a_config_has_only_supported_sections_and_options(self):
+        self.assertEqual(sorted(self.parser.sections()), ["report", "run"])
+        for section in ("run", "report"):
+            for option in self.parser[section]:
+                with self.subTest(section=section, option=option):
+                    self.assertIn(option, _legal_options(section))
+
+    def test_no_other_option_declares_a_measurement_root(self):
+        for option in ("source_pkgs", "source_dirs", "plugins"):
+            with self.subTest(option=option):
+                self.assertNotIn(option, self.parser["run"])
+
     def _raw_floor(self) -> str:
         """Extract fail_under by whole-line partitioning, not by regex.
 
@@ -100,22 +138,8 @@ class CoverageFloorTest(QaTestCase):
         # 7.16.2, all of these are `[report]`-only; it REJECTS exclude_lines, exclude_also
         # and partial_branches under `[run]`, so checking them in both sections is free and
         # forward-looking.
-        for section, keys in (
-            ("run", (OMIT, INCLUDE)),
-            (
-                "report",
-                (
-                    OMIT,
-                    INCLUDE,
-                    "exclude_lines",
-                    "exclude_also",
-                    "partial_branches",
-                    "partial_also",
-                    "partial_branches_always",
-                ),
-            ),
-        ):
-            for key in keys:
+        for section in ("run", "report"):
+            for key in EXCLUSION_KEYS:
                 with self.subTest(section=section, key=key):
                     self.assertNotIn(key, self.parser[section])
 
@@ -153,24 +177,62 @@ class CoverageFloorTest(QaTestCase):
             )
 
     def _assert_source_exclusions(self, tree: Path, *, type_checking_blocks: int = 0):
-        # Statement and partial-branch exemptions can change the gate without a config edit.
-        # The measured tree permits its one import-only TYPE_CHECKING block, but no pragmas
-        # or ellipsis-only bodies. Synthetic consumer trees have no type-only imports.
-        counts = {
-            "coverage pragma": 0,
-            "ellipsis body": 0,
-            "if TYPE_CHECKING:": type_checking_blocks,
-        }
-        found = dict.fromkeys(counts, 0)
-        for path in sorted(tree.rglob("*.py")):
-            for line in path.read_text().splitlines():
-                if PRAGMA.search(line):
-                    found["coverage pragma"] += 1
-                elif ELLIPSIS_BODY.match(line):
-                    found["ellipsis body"] += 1
-                elif TYPE_CHECKING.match(line):
-                    found["if TYPE_CHECKING:"] += 1
-        self.assertEqual(counts, found, "these shrink the denominator without any config change")
+        measured = {Path(path).resolve() for path in find_python_files(str(tree), True)}
+        self.assertEqual(measured, {path.resolve() for path in tree.rglob("*.py")})
+        allowed_blocks = 0
+        for path in sorted(measured):
+            text = path.read_text()
+            self.assertIsNone(PRAGMA.search(text), "coverage pragmas shrink the denominator")
+            baseline = PythonParser(filename=str(path))
+            baseline.parse_source()
+            guarded = PythonParser(filename=str(path), exclude=join_regex(DEFAULT_EXCLUDE))
+            guarded.parse_source()
+            allowed_statements = set()
+            allowed_branches = set()
+            for node in ast.walk(ast.parse(text)):
+                if not isinstance(node, ast.If) or not (
+                    isinstance(node.test, ast.Name) and node.test.id == "TYPE_CHECKING"
+                ):
+                    continue
+                self.assertEqual(path, (MEASURED_TREE / "strategies" / "Utils.py").resolve())
+                self.assertEqual(len(node.body), 1)
+                imported = node.body[0]
+                self.assertIsInstance(imported, ast.ImportFrom)
+                self.assertEqual(imported.module, "xgboost.callback")
+                self.assertEqual(
+                    [(alias.name, alias.asname) for alias in imported.names],
+                    [("TrainingCallback", "XGBoostTrainingCallback")],
+                )
+                self.assertEqual(len(node.orelse), 1)
+                self.assertEqual(
+                    ast.dump(node.orelse[0], include_attributes=False),
+                    ast.dump(ast.parse("XGBoostTrainingCallback = object").body[0]),
+                )
+                allowed_blocks += 1
+                allowed_statements.update(range(node.lineno, imported.end_lineno + 1))
+                allowed_branches.add(node.lineno)
+            with self.subTest(module=path.name):
+                self.assertEqual(
+                    baseline.statements - allowed_statements,
+                    guarded.statements - allowed_statements,
+                )
+                self.assertEqual(
+                    {
+                        line: count
+                        for line, count in _branch_counts(baseline).items()
+                        if line not in allowed_branches
+                    },
+                    {
+                        line: count
+                        for line, count in _branch_counts(guarded).items()
+                        if line not in allowed_branches
+                    },
+                )
+                self.assertEqual(
+                    set(),
+                    baseline.lines_matching(join_regex(DEFAULT_PARTIAL + DEFAULT_PARTIAL_ALWAYS)),
+                )
+        self.assertEqual(allowed_blocks, type_checking_blocks)
 
     def test_no_production_source_widens_coverage_s_default_exclusions(self):
         self._assert_source_exclusions(MEASURED_TREE, type_checking_blocks=1)
@@ -251,5 +313,77 @@ class CoverageFloorTest(QaTestCase):
         )
 
 
+class CoverageGuardRegressionTest(QaTestCase):
+    def _run_guard(self, text, method):
+        with temporary_directory() as directory:
+            fixture = directory / ".coveragerc"
+            fixture.write_text(text)
+            result = unittest.TestResult()
+            with mock.patch.dict(globals(), COVERAGERC=fixture):
+                CoverageFloorTest(method).run(result)
+            self.assertEqual(result.testsRun, 1)
+            self.assertEqual(result.errors, [])
+            return not result.failures
+
+    def test_floor_boundary_uses_actual_guard(self):
+        text = COVERAGERC.read_text()
+        for floor, valid in ((67, False), (68, True)):
+            with self.subTest(floor=floor):
+                changed = re.sub(r"(?m)^fail_under\s*=.*$", f"fail_under = {floor}", text)
+                self.assertEqual(self._run_guard(changed, "test_the_floor_is_never_lowered"), valid)
+
+    def test_config_changes_cannot_shrink_or_replace_the_denominator(self):
+        baseline = COVERAGERC.read_text()
+        for option, method in (
+            ("source_dirs = elsewhere", "test_no_other_option_declares_a_measurement_root"),
+            ("plugins = plugin", "test_no_other_option_declares_a_measurement_root"),
+            ("exclude_also = return", "test_no_production_code_is_excluded_by_any_other_key"),
+            ("exclude_line = return", "test_a_config_has_only_supported_sections_and_options"),
+        ):
+            with self.subTest(option=option):
+                changed = baseline.replace("[run]", f"[run]\n{option}")
+                self.assertFalse(self._run_guard(changed, method))
+        self.assertFalse(
+            self._run_guard(
+                baseline + "\n[unexpected]\n",
+                "test_a_config_has_only_supported_sections_and_options",
+            )
+        )
+
+    def test_provenance_is_dated_final_and_sufficient(self):
+        baseline = COVERAGERC.read_text()
+        for annotation, valid in (
+            ("", False),
+            ("# measured 76.0%", False),
+            ("# measured 2026-10-02", False),
+            ("# measured 2026-10-02 at 67.9%", False),
+            ("# measured 2026-10-02 at 76.0% PROVISIONAL", False),
+            ("# measured 2000-01-01 at 76.0%", True),
+        ):
+            with self.subTest(annotation=annotation):
+                text = "\n".join(
+                    annotation if line.startswith("# measured") else line
+                    for line in baseline.splitlines()
+                )
+                valid_result = self._run_guard(text, "test_the_floor_carries_its_provenance")
+                if valid_result:
+                    valid_result = self._run_guard(
+                        text,
+                        "test_the_provenance_records_a_date_and_a_measurement_that_clears_the_floor",
+                    )
+                self.assertEqual(valid_result, valid)
+
+    def test_default_source_exclusions_are_refused(self):
+        for text in (
+            "def omitted():\n    ...\n",
+            "def choose(flag):\n    if True:\n        return flag\n    return None\n",
+            'def choose(flag):\n    marker = "# pragma: no cover"\n    return flag\n',
+        ):
+            with self.subTest(source=text), temporary_directory() as directory:
+                (directory / "sample.py").write_text(text)
+                with self.assertRaises(AssertionError):
+                    CoverageFloorTest()._assert_source_exclusions(directory)
+
+
 if __name__ == "__main__":
     unittest.main()
index 16c2b55e83b1602138b0c5010ce1952215c96679..320c08546cc57a5694089eebb4e227bb69247b4e 100644 (file)
@@ -87,22 +87,38 @@ class KnownAtLookaheadTest(QaTestCase):
         result = QuickAdapterRegressorV3._known_at_lookahead(unfiltered, unfiltered)
         np.testing.assert_array_equal(result.to_numpy(), np.array([9, 1]))
 
-    def test_a_column_holding_nan_is_skipped_entirely(self):
+    def test_unknown_weight_availability_rejects_only_the_affected_rows(self):
         unfiltered = frame(
-            pd.Index([0, 1]),
-            **{LOOKAHEAD: [3.0, np.nan], WEIGHT_LOOKAHEAD: [9, 9]},
+            pd.Index(range(5)),
+            **{LOOKAHEAD: [1] * 5, WEIGHT_LOOKAHEAD: [100.0, np.nan, 1.0, -2.0, 0.5]},
         )
-        result = QuickAdapterRegressorV3._known_at_lookahead(unfiltered, unfiltered)
-        np.testing.assert_array_equal(result.to_numpy(), np.array([9, 9]))
-
-    def test_a_lone_column_holding_a_nan_leaves_no_usable_lookahead(self):
-        # The skip is per COLUMN, and the fixture above keeps a clean weight column, so the
-        # surviving values coincide whether or not the guard runs. It is observable only
-        # when the ONLY emitted column carries a NaN: the caller must then get None and fall
-        # back to the position-based purge, where without the guard the NaN reaches
-        # astype(np.int64) and raises IntCastingNaNError out of the function.
-        unfiltered = frame(pd.Index([0, 1]), **{LOOKAHEAD: [3.0, np.nan]})
-        self.assertIsNone(QuickAdapterRegressorV3._known_at_lookahead(unfiltered, unfiltered))
+        model = regressor()
+        features = unfiltered.iloc[:, :0]
+
+        available = model._known_before_position_mask(features, unfiltered, PAIR, 5)
+
+        np.testing.assert_array_equal(available, [False, False, True, False, False])
+        self.assertEqual(
+            QuickAdapterRegressorV3._known_at_lookahead(features, unfiltered).iloc[0], 100
+        )
+
+    def test_an_emitted_unknown_label_does_not_fall_back_to_a_short_horizon(self):
+        unfiltered = frame(pd.Index([0, 1, 2]), **{LOOKAHEAD: [0.0, np.nan, 0.0]})
+        model = regressor()
+
+        available = model._known_before_position_mask(unfiltered, unfiltered, PAIR, 3)
+
+        np.testing.assert_array_equal(available, [True, False, True])
+
+    def test_nonfinite_or_overflowing_availability_is_not_mistaken_for_the_past(self):
+        for value in (np.inf, -np.inf, np.iinfo(np.int64).max, np.iinfo(np.uint64).max):
+            with self.subTest(value=value):
+                unfiltered = frame(pd.Index([0, 1]), **{LOOKAHEAD: [value, 0]})
+                model = regressor()
+
+                available = model._known_before_position_mask(unfiltered, unfiltered, PAIR, 2)
+
+                np.testing.assert_array_equal(available, [False, True])
 
     def test_result_is_aligned_to_the_filtered_index(self):
         unfiltered = frame(pd.Index([0, 1, 2]), **{LOOKAHEAD: [5, 6, 7]})
index f50a85131a00f76f166741e0d35f26a365cf9a14..9915e0caaacc3b195cbaa82b17998226fdd9d9ea 100644 (file)
@@ -9,6 +9,7 @@ from unittest import mock
 
 import numpy as np
 import optuna
+import Utils
 from EnumErrors import enum_error_message
 from optuna.distributions import FloatDistribution, IntDistribution
 from optuna.trial import FrozenTrial, TrialState
@@ -162,6 +163,57 @@ def seed_trial(
     return target
 
 
+class QuarantineCollisionTest(QaTestCase):
+    def test_real_callers_skip_ninth_collision_and_refuse_exhausted_targets(self):
+        now = datetime.datetime(2026, 1, 1, tzinfo=datetime.UTC)
+        stamp = now.strftime("%Y%m%dT%H%M%S%fZ")
+        for kind in ("journal", "best_params"):
+            for occupied in (9, 100):
+                with self.subTest(kind=kind, occupied=occupied), temporary_directory() as directory:
+                    source = directory / ("optuna.log" if kind == "journal" else "best.json")
+                    source.write_text("corrupt payload")
+                    targets = []
+                    for index in range(occupied):
+                        suffix = "" if index == 0 else f"-{index}"
+                        target = source.with_name(f"{source.name}.corrupt-{stamp}{suffix}")
+                        target.write_text(f"preserved {index}")
+                        targets.append(target)
+                    clock_target = (
+                        f"{REGRESSOR.__module__}.datetime"
+                        if kind == "journal"
+                        else "Utils.datetime"
+                    )
+                    with mock.patch(clock_target) as clock:
+                        clock.now.return_value = now
+                        if kind == "journal":
+
+                            def call(source=source):
+                                return REGRESSOR._optuna_quarantine_journal(
+                                    source, PAIR, ValueError("corrupt")
+                                )
+
+                        else:
+
+                            def call(source=source):
+                                return Utils._quarantine_corrupt_optuna_best_params(
+                                    source, PAIR, "hp", ValueError("corrupt"), None
+                                )
+
+                        if occupied == 100:
+                            with self.assertRaises(FileExistsError):
+                                call()
+                            self.assertEqual(source.read_text(), "corrupt payload")
+                        else:
+                            result = call()
+                            self.assertEqual(result.name, f"{source.name}.corrupt-{stamp}-9")
+                            self.assertEqual(result.read_text(), "corrupt payload")
+                            self.assertFalse(source.exists())
+                    self.assertEqual(
+                        [target.read_text() for target in targets],
+                        [f"preserved {index}" for index in range(occupied)],
+                    )
+
+
 class RegressorOptunaStudyTest(QaTestCase):
     # ------------------------------------------------------------------ direction guards
 
diff --git a/quickadapter/tests/test_regressor_pipeline_contracts.py b/quickadapter/tests/test_regressor_pipeline_contracts.py
new file mode 100644 (file)
index 0000000..8706149
--- /dev/null
@@ -0,0 +1,1075 @@
+"""Prediction validity and transport contracts in the native Freqtrade QA image."""
+
+import importlib
+import json
+import sys
+import unittest
+from pathlib import Path
+from types import SimpleNamespace
+from unittest.mock import patch
+
+import numpy as np
+import optuna
+import pandas as pd
+from freqtrade.enums import RunMode
+from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
+from freqtrade.plot.plotting import add_indicators
+from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
+from freqtrade.resolvers.strategy_resolver import StrategyResolver
+from freqtrade.rpc.rpc import RPC
+from plotly.subplots import make_subplots
+from qa_support import (
+    CONFIG_TEMPLATE,
+    PAIR,
+    QaTestCase,
+    _merge,
+    model_config,
+    ohlcv_frame,
+    temporary_directory,
+)
+from QuickAdapterV3 import QuickAdapterV3
+
+from quickadapter.user_data.freqaimodels.QuickAdapterRegressorV3 import (
+    _PRODUCED_PREDICTION_COLUMN,
+    QuickAdapterRegressorV3,
+)
+
+LABEL = "&s-extrema"
+
+
+def trained_regressor(directory: Path, di_threshold: float = 0):
+    config = model_config(
+        directory,
+        freqai={
+            "continual_learning": False,
+            "optuna_hyperopt": {"enabled": False},
+            "feature_parameters": {"DI_threshold": di_threshold, "causal_mode": True},
+            "data_split_parameters": {"test_size": 0.2, "shuffle": False},
+            "model_training_parameters": {"n_estimators": 3, "n_jobs": 1, "verbosity": 0},
+        },
+    )
+    position = np.arange(120)
+    frame = pd.DataFrame(
+        {
+            "date": pd.date_range("2026-01-01", periods=len(position), freq="5min", tz="UTC"),
+            "%-x": np.sin(position / 5),
+            "%-lag": np.cos(position / 7),
+            LABEL: 0.7 * np.sin(position / 5) + 0.2 * np.cos(position / 7),
+        }
+    )
+    model = QuickAdapterRegressorV3(config=config)
+    model.live = True
+    kitchen = FreqaiDataKitchen(config, live=True, pair=PAIR)
+    kitchen.label_list = [LABEL]
+    kitchen.training_features_list = ["%-x", "%-lag"]
+    timestamp = int(frame["date"].iloc[-1].timestamp()) + 300
+    kitchen.set_paths(PAIR, timestamp)
+    kitchen.set_new_model_names(PAIR, timestamp)
+    kitchen.data_path.mkdir(parents=True, exist_ok=True)
+    model.model = model.train(frame, PAIR, kitchen)
+    return model, kitchen, frame
+
+
+class PredictionValidityTest(QaTestCase):
+    def test_nonfinite_features_cannot_be_admitted_after_native_preprocessing(self):
+        for threshold, expected in ((0, [1, 0, 1]), (10, [1, 0, 1])):
+            with self.subTest(di_threshold=threshold), temporary_directory() as directory:
+                model, kitchen, frame = trained_regressor(directory, threshold)
+                for missing in (np.nan, np.inf, -np.inf):
+                    with self.subTest(missing=missing):
+                        inputs = frame.iloc[30:33].copy()
+                        inputs.iloc[1, inputs.columns.get_loc("%-x")] = missing
+
+                        predictions, status = model.predict(inputs, kitchen)
+
+                        self.assertEqual(
+                            kitchen.data["filter_drop_index_prediction"].tolist(),
+                            [False, True, False],
+                        )
+                        np.testing.assert_array_equal(status, expected)
+                        self.assertTrue(np.isfinite(predictions[LABEL]).all())
+
+    def test_missing_features_and_native_di_rejections_accumulate(self):
+        with temporary_directory() as directory:
+            model, kitchen, frame = trained_regressor(directory, 10)
+            inputs = frame.iloc[30:33].copy()
+            inputs.iloc[1, inputs.columns.get_loc("%-x")] = np.nan
+            inputs.iloc[1, inputs.columns.get_loc("%-lag")] = 1e6
+
+            _, status = model.predict(inputs, kitchen)
+
+            np.testing.assert_array_equal(status, [1, -1, 1])
+            np.testing.assert_array_equal(
+                kitchen.__dict__["_quickadapter_prediction_produced"], [True, False, True]
+            )
+
+    def test_nonfinite_backend_outputs_cannot_enter_calibration(self):
+        for threshold in (0, 10):
+            with self.subTest(di_threshold=threshold), temporary_directory() as directory:
+                model, kitchen, training = trained_regressor(directory, threshold)
+                native_predict = model.model.predict
+                encoded_zero, _, _ = kitchen.label_pipeline.transform(pd.DataFrame({LABEL: [0.0]}))
+                for value in (0.0, np.nan, np.inf, -np.inf):
+                    for missing in (False, True):
+                        with self.subTest(output=value, missing_input=missing):
+                            inputs = training.iloc[30:33].copy()
+                            if missing:
+                                inputs.iloc[1, inputs.columns.get_loc("%-x")] = np.nan
+                            if threshold:
+                                inputs.iloc[1, inputs.columns.get_loc("%-lag")] = 1e6
+                            backend_value = encoded_zero[LABEL].iloc[0] if value == 0 else value
+
+                            def damaged(
+                                features,
+                                *args,
+                                replacement=backend_value,
+                                native=native_predict,
+                                **kwargs,
+                            ):
+                                values = np.array(native(features, *args, **kwargs), copy=True)
+                                values[1] = replacement
+                                return values
+
+                            with patch.object(model.model, "predict", side_effect=damaged):
+                                predictions, status = model.predict(inputs, kitchen)
+
+                            invalid = missing or not np.isfinite(value)
+                            expected_status = 1 - int(bool(threshold)) - int(invalid)
+                            np.testing.assert_array_equal(status, [1, expected_status, 1])
+                            produced = kitchen.__dict__["_quickadapter_prediction_produced"]
+                            np.testing.assert_array_equal(produced, [True, not invalid, True])
+                            history = pd.DataFrame(
+                                {
+                                    "date_pred": inputs["date"].to_numpy(),
+                                    LABEL: predictions[LABEL].to_numpy(),
+                                    "do_predict": status,
+                                    _PRODUCED_PREDICTION_COLUMN: produced,
+                                }
+                            )
+                            model._calibration_current_candles[PAIR] = inputs["date"].iloc[
+                                -1
+                            ] + pd.Timedelta(minutes=5)
+                            eligible = model._calibration_history(kitchen, PAIR, history, 3)
+                            expected_dates = inputs["date"].iloc[[0, 2] if invalid else [0, 1, 2]]
+                            self.assertEqual(
+                                eligible["date_pred"].tolist(), expected_dates.tolist()
+                            )
+                            public = model._normalise_prediction_returns(history.copy())
+                            np.testing.assert_allclose(
+                                public["prediction_available"], [1, 0 if invalid else 1, 1]
+                            )
+                            if invalid:
+                                self.assertTrue(pd.isna(public[LABEL].iloc[1]))
+                            else:
+                                self.assertAlmostEqual(public[LABEL].iloc[1], 0.0)
+
+    def test_loaded_model_metric_survives_missing_history_and_bootstrap(self):
+        with temporary_directory() as directory:
+            model, kitchen, frame = trained_regressor(directory)
+            model.dd.get_pair_dict_info(PAIR)
+            for stored, expected in ((0.25, 0.25), (0, 0), (np.inf, np.inf), (-1, np.inf)):
+                with self.subTest(stored=stored):
+                    kitchen.data["extra_returns_per_train"]["holdout_rmse"] = stored
+                    model.dd.save_data(model.model, PAIR, kitchen)
+                    model.dd.save_drawer_to_disk()
+                    restored = QuickAdapterRegressorV3(config=model.config)
+                    restored.live = True
+                    loaded_kitchen = FreqaiDataKitchen(model.config, live=True, pair=PAIR)
+                    restored.model = restored.dd.load_data(PAIR, loaded_kitchen)
+                    inputs = frame.iloc[30:33].copy()
+                    predictions, _ = restored.predict(inputs, loaded_kitchen)
+                    strategy = ohlcv_frame(np.full(len(inputs), 100.0))
+                    strategy["date"] = inputs["date"].to_numpy()
+                    restored.set_initial_historic_predictions(
+                        predictions, loaded_kitchen, PAIR, strategy
+                    )
+                    restored.fit_live_predictions(loaded_kitchen, PAIR)
+
+                    self.assertEqual(
+                        loaded_kitchen.data["extra_returns_per_train"]["holdout_rmse"], expected
+                    )
+                    self.assertEqual(
+                        restored.dd.historic_predictions[PAIR][
+                            _PRODUCED_PREDICTION_COLUMN
+                        ].tolist(),
+                        [False] * len(inputs),
+                    )
+
+
+class PredictionTransportTest(QaTestCase):
+    def test_restart_keeps_unknown_rmse_and_ambiguous_legacy_zero(self):
+        with temporary_directory() as directory:
+            model = QuickAdapterRegressorV3(
+                config=model_config(directory, freqai={"optuna_hyperopt": {"enabled": False}})
+            )
+            dates = pd.date_range("2026-01-01", periods=3, freq="5min", tz="UTC")
+            strategy = pd.DataFrame({"date": dates, "close": [100.0] * 3})
+            predictions = pd.DataFrame({LABEL: [0.0, 0.0, 0.2]})
+            model.dd.historic_predictions[PAIR] = pd.DataFrame(
+                {
+                    "date_pred": dates,
+                    LABEL: predictions[LABEL],
+                    "do_predict": [0, 0, 1],
+                    "holdout_rmse": [0.0, np.nan, np.nan],
+                    _PRODUCED_PREDICTION_COLUMN: [True, np.nan, True],
+                }
+            )
+
+            model.dd.save_historic_predictions_to_disk()
+            restored = QuickAdapterRegressorV3(config=model.config)
+            restored.dd.set_initial_return_values(PAIR, predictions, strategy)
+            result = restored._normalise_prediction_returns(restored.dd.model_return_values[PAIR])
+
+            np.testing.assert_allclose(result[LABEL], [0.0, 0.0, 0.2])
+            np.testing.assert_allclose(result["prediction_available"], [1, np.nan, 1])
+            np.testing.assert_allclose(result["holdout_rmse"], [0, np.nan, np.nan])
+            np.testing.assert_allclose(
+                restored.dd.historic_predictions[PAIR]["holdout_rmse"], [0, np.nan, np.nan]
+            )
+
+    def test_restored_public_returns_distinguish_absence_legacy_and_real_zero(self):
+        with temporary_directory() as directory:
+            model, kitchen, training = trained_regressor(directory)
+            inputs = training.iloc[30:34].copy()
+            predictions, _ = model.predict(inputs, kitchen)
+            strategy = ohlcv_frame(np.full(len(inputs), 100.0))
+            strategy["date"] = inputs["date"].to_numpy()
+            strategy["%-x"] = inputs["%-x"].to_numpy()
+            strategy["%-lag"] = inputs["%-lag"].to_numpy()
+            model.dd.get_pair_dict_info(PAIR)
+            model.dd.save_data(model.model, PAIR, kitchen)
+            model.dd.save_drawer_to_disk()
+            model.dd.historic_predictions[PAIR] = pd.DataFrame(
+                {
+                    "date_pred": strategy["date"].iloc[1:].to_numpy(),
+                    LABEL: [0.0, 0.0, predictions[LABEL].iloc[-1]],
+                    "do_predict": [0, 0, 1],
+                    _PRODUCED_PREDICTION_COLUMN: [np.nan, True, True],
+                }
+            )
+            model.dd.save_historic_predictions_to_disk()
+            restored = QuickAdapterRegressorV3(config=model.config)
+            restored.live = True
+            loaded = FreqaiDataKitchen(model.config, live=True, pair=PAIR)
+            restored.model = restored.dd.load_data(PAIR, loaded)
+
+            restored.build_strategy_return_arrays(strategy, loaded, PAIR, 0)
+
+            result = loaded.return_dataframe
+            np.testing.assert_allclose(result["prediction_available"], [0, np.nan, 1, 1])
+            np.testing.assert_allclose(
+                result[LABEL], [np.nan, 0.0, 0.0, predictions[LABEL].iloc[-1]]
+            )
+            self.assertEqual(result["date"].tolist(), strategy["date"].tolist())
+            payload = RPC._convert_dataframe_to_dict(
+                "QuickAdapterV3",
+                PAIR,
+                "5m",
+                result,
+                strategy["date"].iloc[-1].to_pydatetime(),
+                None,
+                [],
+            )
+            availability = payload["columns"].index("prediction_available")
+            prediction = payload["columns"].index(LABEL)
+            self.assertEqual([row[availability] for row in payload["data"]], [0, None, 1, 1])
+            self.assertEqual([row[prediction] for row in payload["data"]][:3], [None, 0.0, 0.0])
+            self.assertNotIn(_PRODUCED_PREDICTION_COLUMN, payload["columns"])
+
+    def test_historic_live_model_replay_distinguishes_missing_and_legacy_rows(self):
+        for with_marker in (False, True):
+            with self.subTest(with_marker=with_marker), temporary_directory() as directory:
+                config = model_config(directory, freqai={"optuna_hyperopt": {"enabled": False}})
+                config.update(
+                    runmode=RunMode.BACKTEST,
+                    trading_mode="spot",
+                    timerange="20240101-20240102",
+                    config_files=[str(CONFIG_TEMPLATE)],
+                    freqai_backtest_live_models=True,
+                )
+                frame = ohlcv_frame([100.0, 101.0, 102.0, 103.0])
+                frame["date"] = pd.to_datetime(frame["date"], utc=True)
+                history = pd.DataFrame(
+                    {
+                        "date_pred": frame["date"].iloc[[0, 2, 3]].to_numpy(),
+                        LABEL: [0.0, 0.5, 0.0],
+                        "do_predict": [1, 1, 0],
+                        "holdout_rmse": [0.2] * 3,
+                    }
+                )
+                if with_marker:
+                    history[_PRODUCED_PREDICTION_COLUMN] = [True, True, np.nan]
+                model = QuickAdapterRegressorV3(config=config)
+                model.dd.historic_predictions[PAIR] = history
+                model.dd.save_historic_predictions_to_disk()
+                restored = QuickAdapterRegressorV3(config=config)
+                strategy = QuickAdapterV3(config)
+                strategy.freqai = restored
+                strategy.freqai_info = config["freqai"]
+                strategy.bot_start()
+                strategy.dp = SimpleNamespace(runmode=RunMode.BACKTEST)
+
+                result = restored.start(frame, {"pair": PAIR}, strategy)
+
+                np.testing.assert_allclose(result["prediction_available"], [1, 0, 1, np.nan])
+                np.testing.assert_allclose(result[LABEL], [0.0, np.nan, 0.5, 0.0])
+                np.testing.assert_allclose(result["holdout_rmse"], [0.2, np.nan, 0.2, 0.2])
+                np.testing.assert_allclose(result["do_predict"], [1, np.nan, 1, 0])
+                pd.testing.assert_frame_equal(restored.dd.historic_predictions[PAIR], history)
+                payload = RPC._convert_dataframe_to_dict(
+                    "QuickAdapterV3",
+                    PAIR,
+                    "5m",
+                    result,
+                    frame["date"].iloc[-1].to_pydatetime(),
+                    None,
+                    [],
+                )
+                availability = payload["columns"].index("prediction_available")
+                self.assertEqual([row[availability] for row in payload["data"]], [1, 0, 1, None])
+                self.assertNotIn(_PRODUCED_PREDICTION_COLUMN, payload["columns"])
+
+    def test_old_history_accepts_new_metrics_without_fabricating_past_scores(self):
+        for flag in (True, False, np.bool_(True), np.bool_(False)):
+            with self.subTest(flag=flag), temporary_directory() as directory:
+                config = model_config(directory, freqai={"optuna_hyperopt": {"enabled": False}})
+                model = QuickAdapterRegressorV3(config=config)
+                model.live = True
+                kitchen = FreqaiDataKitchen(config, live=True, pair=PAIR)
+                kitchen.label_list = [LABEL]
+                kitchen.data.update(
+                    labels_mean={LABEL: 0.0},
+                    labels_std={LABEL: 0.1},
+                    extra_returns_per_train={
+                        "holdout_rmse": 0.25,
+                        "label_period_candles": 1,
+                        "has_signal": flag,
+                    },
+                )
+                dates = pd.date_range("2026-01-01", periods=3, freq="5min", tz="UTC")
+                model.dd.historic_predictions[PAIR] = pd.DataFrame(
+                    {
+                        "date_pred": dates[:2],
+                        LABEL: [0.1, 0.0],
+                        "do_predict": [1, 0],
+                        "close_price": [100.0, 100.0],
+                        "high_price": [101.0, 101.0],
+                        "low_price": [99.0, 99.0],
+                        _PRODUCED_PREDICTION_COLUMN: [True, True],
+                    }
+                )
+                strategy = pd.DataFrame(
+                    {"date": dates, "high": [101.0] * 3, "low": [99.0] * 3, "close": [100.0] * 3}
+                )
+
+                model.dd.append_model_predictions(
+                    PAIR, pd.DataFrame({LABEL: [0.0]}), np.array([0]), kitchen, strategy
+                )
+                model.dd.save_historic_predictions_to_disk()
+                restart_strategy = pd.concat(
+                    [
+                        strategy,
+                        strategy.iloc[-1:].assign(date=dates[-1] + pd.Timedelta(minutes=5)),
+                    ],
+                    ignore_index=True,
+                )
+                restored = QuickAdapterRegressorV3(config=config)
+                restored.dd.set_initial_return_values(
+                    PAIR, pd.DataFrame({LABEL: np.zeros(4)}), restart_strategy
+                )
+                result = restored._normalise_prediction_returns(
+                    restored.dd.model_return_values[PAIR]
+                )
+
+                np.testing.assert_allclose(result["holdout_rmse"], [np.nan, np.nan, 0.25, np.nan])
+                np.testing.assert_allclose(result["prediction_available"], [1, 1, 1, 0])
+                np.testing.assert_allclose(result[LABEL], [0.1, 0.0, 0.0, np.nan])
+                self.assertTrue(result["has_signal"].iloc[:2].isna().all())
+                self.assertEqual(result["has_signal"].dtype, object)
+                self.assertIsInstance(result["has_signal"].iloc[2], (bool, np.bool_))
+                self.assertEqual(result["has_signal"].iloc[2], flag)
+
+    def test_native_no_model_return_does_not_expose_zero_predictions(self):
+        with temporary_directory() as directory:
+            fields = ("DI_cutoff", "DI_value_param1", "DI_value_param2", "DI_value_param3")
+            config = model_config(
+                directory,
+                freqai={
+                    "optuna_hyperopt": {"enabled": False},
+                    "extra_returns_per_train": dict.fromkeys(fields, 2.0),
+                },
+            )
+            model = QuickAdapterRegressorV3(config=config)
+            kitchen = FreqaiDataKitchen(config, live=True, pair=PAIR)
+            frame = ohlcv_frame(np.full(3, 100.0))
+            frame["%-feature"] = [0.1, 0.2, 0.3]
+            frame[LABEL] = [0.5, 0.0, -0.5]
+
+            model.dd.return_null_values_to_strategy(frame, kitchen)
+
+            np.testing.assert_array_equal(kitchen.return_dataframe["do_predict"], [0, 0, 0])
+            np.testing.assert_array_equal(
+                kitchen.return_dataframe["prediction_available"], [0, 0, 0]
+            )
+            self.assertTrue(kitchen.return_dataframe[LABEL].isna().all())
+            self.assertTrue(kitchen.return_dataframe["holdout_rmse"].isna().all())
+            for column in fields:
+                self.assertTrue(kitchen.return_dataframe[column].isna().all())
+            payload = RPC._convert_dataframe_to_dict(
+                "QuickAdapterV3",
+                PAIR,
+                "5m",
+                kitchen.return_dataframe.copy(),
+                frame["date"].iloc[-1].to_pydatetime(),
+                None,
+                [],
+            )
+            for column in fields:
+                position = payload["columns"].index(column)
+                self.assertEqual([row[position] for row in payload["data"]], [None] * 3)
+
+    def test_di_diagnostics_keep_real_and_legacy_zeros_but_not_expired_values(self):
+        with temporary_directory() as directory:
+            model = QuickAdapterRegressorV3(
+                config=model_config(directory, freqai={"optuna_hyperopt": {"enabled": False}})
+            )
+            fields = ("DI_cutoff", "DI_value_param1", "DI_value_param2", "DI_value_param3")
+            frame = pd.DataFrame(
+                {
+                    "do_predict": [0, 1, 2, 0],
+                    LABEL: [0.0] * 4,
+                    _PRODUCED_PREDICTION_COLUMN: [True, True, True, np.nan],
+                    **dict.fromkeys(fields, 0.0),
+                }
+            )
+
+            result = model._normalise_prediction_returns(frame)
+
+            self.assertEqual(result["do_predict"].tolist(), [0, 1, 2, 0])
+            np.testing.assert_allclose(result["prediction_available"], [1, 1, 0, np.nan])
+            for column in (LABEL, *fields):
+                np.testing.assert_allclose(result[column], [0, 0, np.nan, 0])
+
+    def test_native_plotly_draws_real_zero_and_missing_prediction_gaps(self):
+        with temporary_directory() as directory:
+            config = model_config(directory, freqai={"optuna_hyperopt": {"enabled": False}})
+            model = QuickAdapterRegressorV3(config=config)
+            strategy = QuickAdapterV3(config)
+            dates = pd.date_range("2026-01-01", periods=5, freq="5min", tz="UTC")
+            frame = pd.DataFrame(
+                {
+                    "date": dates,
+                    LABEL: [0, 0, 0, 0, 0.5],
+                    "holdout_rmse": [0, 0, -1, np.inf, 0.25],
+                    "do_predict": [0, 0, 1, 2, 1],
+                    _PRODUCED_PREDICTION_COLUMN: [True, False, True, True, True],
+                }
+            )
+            for column in (
+                "extrema_direction",
+                "extrema_direction_smoothed",
+                "extrema_weight",
+                "extrema_weight_smoothed",
+            ):
+                frame[column] = 0.0
+            result = model._normalise_prediction_returns(frame)
+            figure = make_subplots(rows=len(strategy.plot_config["subplots"]), cols=1)
+            for row, indicators in enumerate(strategy.plot_config["subplots"].values(), start=1):
+                add_indicators(figure, row, indicators, result)
+            traces = {trace.name: trace for trace in figure.data}
+
+            np.testing.assert_allclose(traces[LABEL].y, [0, np.nan, 0, np.nan, 0.5])
+            np.testing.assert_allclose(traces["holdout_rmse"].y, [0, np.nan, np.nan, np.nan, 0.25])
+            np.testing.assert_array_equal(traces["extrema_direction_smoothed"].y, [0] * 5)
+            self.assertEqual(traces[LABEL].type, "scatter")
+            self.assertEqual(traces[LABEL].mode, "lines")
+            self.assertEqual(traces["holdout_rmse"].mode, "lines")
+            self.assertEqual(traces["extrema_direction_smoothed"].mode, "lines")
+            self.assertEqual(traces["extrema_weight_smoothed"].mode, "lines")
+            self.assertEqual(traces["extrema_direction"].type, "bar")
+            self.assertEqual(traces["extrema_weight"].type, "bar")
+
+
+class BacktestContractsTest(QaTestCase):
+    def test_replay_uses_produced_observations_and_current_model_metric(self):
+        with temporary_directory() as directory:
+            config = model_config(directory, freqai={"optuna_hyperopt": {"enabled": False}})
+            model = QuickAdapterRegressorV3(config=config)
+            model.live = False
+            dates = pd.date_range("2026-01-01", periods=6, freq="5min", tz="UTC")
+            frame = pd.DataFrame(
+                {
+                    "date": dates,
+                    LABEL: [0.8, 0.6, 0, 0.4, 0.2, 0.0],
+                    "do_predict": [1, 0, 0, 1, 1, 1],
+                    "holdout_rmse": [0.8, 0.6, np.nan, 0.4, 0.2, 0.0],
+                    _PRODUCED_PREDICTION_COLUMN: [True, True, False, True, True, True],
+                }
+            )
+            kitchen = SimpleNamespace(
+                pair=PAIR,
+                label_list=[LABEL],
+                unique_class_list=[],
+                full_df=frame,
+                data={"extra_returns_per_train": {}},
+            )
+
+            model.backtesting_fit_live_predictions(kitchen)
+
+            self.assertAlmostEqual(frame.loc[4, f"{LABEL}_mean"], 0.5)
+            self.assertAlmostEqual(frame.loc[4, f"{LABEL}_std"], 0.1)
+            self.assertEqual(frame["holdout_rmse"].tolist()[-3:], [0.4, 0.2, 0])
+            # Current candle must not influence its own calibration.
+            self.assertAlmostEqual(frame.loc[5, f"{LABEL}_mean"], 0.3)
+
+    def test_replay_and_live_reset_warmup_after_the_same_long_gap(self):
+        with temporary_directory() as directory:
+            config = model_config(directory, freqai={"optuna_hyperopt": {"enabled": False}})
+            frame = pd.DataFrame(
+                {
+                    "date": pd.to_datetime(
+                        [
+                            "2026-01-01 00:00",
+                            "2026-01-01 00:05",
+                            "2026-01-01 00:25",
+                            "2026-01-01 00:30",
+                            "2026-01-01 00:35",
+                            "2026-01-01 00:40",
+                        ],
+                        utc=True,
+                    ),
+                    LABEL: [0.8, 0.6, 0.0, 0.4, 0.2, 0.0],
+                    "do_predict": [1, 0, 0, 1, 1, 1],
+                    _PRODUCED_PREDICTION_COLUMN: [True, True, False, True, True, True],
+                }
+            )
+            replay = QuickAdapterRegressorV3(config=config)
+            replay.live = False
+            kitchen = SimpleNamespace(
+                pair=PAIR,
+                label_list=[LABEL],
+                unique_class_list=[],
+                full_df=frame.copy(),
+                data={"extra_returns_per_train": {}},
+            )
+            replay.backtesting_fit_live_predictions(kitchen)
+            live = QuickAdapterRegressorV3(config=config)
+            live.live = True
+            for position in range(len(frame)):
+                live.dd.historic_predictions[PAIR] = frame.iloc[:position].rename(
+                    columns={"date": "date_pred"}
+                )
+                live._calibration_current_candles[PAIR] = frame["date"].iloc[position]
+                live.fit_live_predictions(kitchen, PAIR)
+                self.assertAlmostEqual(
+                    kitchen.data["labels_mean"][LABEL],
+                    kitchen.full_df.loc[position, f"{LABEL}_mean"],
+                )
+                self.assertAlmostEqual(
+                    kitchen.data["labels_std"][LABEL],
+                    kitchen.full_df.loc[position, f"{LABEL}_std"],
+                )
+            np.testing.assert_allclose(kitchen.full_df.loc[2:4, f"{LABEL}_mean"], 0)
+            self.assertAlmostEqual(kitchen.full_df.loc[5, f"{LABEL}_mean"], 0.3)
+
+    @staticmethod
+    def _run_backtest(
+        directory: Path,
+        frame: pd.DataFrame,
+        weighting: str = "uniform",
+        *,
+        strategy_name: str = "QuickAdapterV3",
+        model_name: str = "QuickAdapterRegressorV3",
+        strategy_path: Path | None = None,
+        model_path: Path | None = None,
+        freqai_overrides: dict | None = None,
+        config_overrides: dict | None = None,
+    ):
+        frame = frame.copy()
+        frame["date"] = pd.to_datetime(frame["date"], utc=True)
+        config = model_config(
+            directory,
+            freqai=_merge(
+                {
+                    "continual_learning": False,
+                    "save_backtest_models": True,
+                    "activate_tensorboard": False,
+                    "fit_live_predictions_candles": 12,
+                    "label_weighting": {"strategy": weighting},
+                    "optuna_hyperopt": {"enabled": False},
+                    "feature_parameters": {
+                        "indicator_periods_candles": [4, 8],
+                        "include_shifted_candles": 0,
+                        "buffer_train_data_candles": 0,
+                        "causal_mode": True,
+                    },
+                    "data_split_parameters": {"test_size": 0.2, "shuffle": False},
+                    "model_training_parameters": {
+                        "n_estimators": 3,
+                        "n_jobs": 1,
+                        "verbosity": 0,
+                        "random_state": 7,
+                    },
+                },
+                freqai_overrides or {},
+            ),
+        )
+        config.update(
+            runmode=RunMode.BACKTEST,
+            timerange="20240102-20240103",
+            config_files=[str(CONFIG_TEMPLATE)],
+            strategy=strategy_name,
+            freqaimodel=model_name,
+            freqaimodel_path=str(model_path or CONFIG_TEMPLATE.parent / "freqaimodels"),
+        )
+        config = _merge(config, config_overrides or {})
+        model = FreqaiModelResolver.load_freqaimodel(config)
+        model.live = False
+        strategy = StrategyResolver._load_strategy(
+            strategy_name,
+            config,
+            extra_dir=str(strategy_path or CONFIG_TEMPLATE.parent / "strategies"),
+        )
+        strategy.freqai = model
+        strategy.freqai_info = config["freqai"]
+        strategy.bot_start()
+        kitchen = FreqaiDataKitchen(config, live=False, pair=PAIR)
+        model.dk = kitchen
+        model.start_backtesting(frame, {"pair": PAIR}, kitchen, strategy)
+        kitchen.return_dataframe = kitchen.remove_features_from_df(kitchen.return_dataframe)
+        return kitchen
+
+    def test_replay_returns_effective_parameters_in_fresh_and_cached_backtests(self):
+        with temporary_directory() as directory:
+            frame = ohlcv_frame(
+                100 + 8 * np.sin(np.arange(720) / 6) + 2 * np.sin(np.arange(720) / 23)
+            )
+            overrides = {
+                "feature_parameters": {"label_period_candles": 24, "label_natr_multiplier": 7.0},
+                "extra_returns_per_train": {
+                    "label_period_candles": 18,
+                    "label_natr_multiplier": 10.5,
+                },
+            }
+            for cached in (False, True):
+                with self.subTest(cached=cached):
+                    kitchen = self._run_backtest(directory, frame, freqai_overrides=overrides)
+                    result = kitchen.return_dataframe
+                    produced = result.loc[result["prediction_available"].eq(1)]
+                    np.testing.assert_array_equal(produced["label_period_candles"], 24)
+                    np.testing.assert_array_equal(produced["label_natr_multiplier"], 7.0)
+                    self.assertEqual(
+                        produced["date"].tolist(),
+                        pd.date_range("2024-01-02", periods=288, freq="5min", tz="UTC").tolist(),
+                    )
+
+    def test_real_label_hpo_replay_matches_live_parameters_and_keeps_window_rmse(self):
+        with temporary_directory() as directory:
+            config = model_config(
+                directory,
+                freqai={
+                    "fit_live_predictions_candles": 864,
+                    "feature_parameters": {
+                        "label_period_candles": 18,
+                        "label_natr_multiplier": 10.5,
+                    },
+                    "optuna_hyperopt": {
+                        "enabled": False,
+                        "n_trials": 1,
+                        "n_jobs": 1,
+                        "timeout": 30,
+                        "warm_start": False,
+                    },
+                },
+            )
+            config.update(timerange="20240102-20240103", config_files=[str(CONFIG_TEMPLATE)])
+            model = QuickAdapterRegressorV3(config=config)
+            model.live = False
+            model._optuna_hyperopt = True
+            model._optuna_label_candle[PAIR] = 0
+            raw = ohlcv_frame(100 + 4 * np.sin(np.arange(600) / 4))
+            raw["date"] = pd.to_datetime(raw["date"], utc=True)
+            model.data_provider = SimpleNamespace(get_pair_dataframe=lambda **kwargs: raw)
+            chosen = {
+                "label_period_candles": model._min_label_period_candles,
+                "label_natr_multiplier": model._min_label_natr_multiplier,
+            }
+
+            def create_study(**kwargs):
+                study = optuna.create_study(
+                    directions=kwargs["directions"], sampler=optuna.samplers.RandomSampler(seed=7)
+                )
+                study.enqueue_trial(chosen)
+                return study
+
+            kitchen = FreqaiDataKitchen(config, live=False, pair=PAIR)
+            kitchen.label_list = [LABEL]
+            kitchen.full_df = raw.iloc[500:504][["date"]].reset_index(drop=True)
+            kitchen.full_df[LABEL] = [-0.2, 0.1, 0.4, -0.3]
+            kitchen.full_df["do_predict"] = 1
+            kitchen.full_df[_PRODUCED_PREDICTION_COLUMN] = True
+            kitchen.full_df["holdout_rmse"] = [0.1, 0.2, 0.3, 0.4]
+            kitchen.full_df["label_period_candles"] = 18
+            kitchen.full_df["label_natr_multiplier"] = 10.5
+            model.dd.get_pair_dict_info(PAIR)
+
+            with patch.object(model, "optuna_create_study", side_effect=create_study):
+                model.backtesting_fit_live_predictions(kitchen)
+
+            self.assertEqual(model.get_optuna_params(PAIR, "label"), chosen)
+            for column, value in chosen.items():
+                np.testing.assert_array_equal(kitchen.full_df[column], value)
+            np.testing.assert_allclose(kitchen.full_df["holdout_rmse"], [0.1, 0.2, 0.3, 0.4])
+            model.live = True
+            model._optuna_hyperopt = False
+            model.fit_live_predictions(kitchen, PAIR)
+            for column, value in chosen.items():
+                self.assertEqual(kitchen.data["extra_returns_per_train"][column], value)
+
+    def test_external_target_input_change_isolates_models_and_predictions(self):
+        with temporary_directory() as directory:
+            implementations = directory / "implementations"
+            implementations.mkdir()
+            (implementations / "funding_strategy.py").write_text(
+                "from QuickAdapterV3 import QuickAdapterV3\n"
+                "class FundingStrategy(QuickAdapterV3):\n"
+                "    def set_freqai_targets(self, dataframe, metadata, **kwargs):\n"
+                "        result = super().set_freqai_targets(dataframe, metadata, **kwargs)\n"
+                "        result['&s-extrema'] *= result['funding_rate']\n"
+                "        return result\n"
+            )
+            frame = ohlcv_frame(
+                100 + 8 * np.sin(np.arange(720) / 6) + 2 * np.sin(np.arange(720) / 23)
+            )
+            frame["funding_rate"] = 1.0
+            frame["sentiment"] = pd.Series(["neutral"] * len(frame), dtype="string")
+            options = {"strategy_name": "FundingStrategy", "strategy_path": implementations}
+            first = self._run_backtest(directory / "shared", frame, **options)
+            frame["funding_rate"] = -1.0
+
+            changed = self._run_backtest(directory / "shared", frame, **options)
+            fresh = self._run_backtest(directory / "fresh", frame, **options)
+            cached = self._run_backtest(directory / "shared", frame, **options)
+
+            self.assertNotEqual(first.data_path.parent, changed.data_path.parent)
+            self.assertEqual(changed.data_path.parent, cached.data_path.parent)
+            pd.testing.assert_frame_equal(changed.return_dataframe, fresh.return_dataframe)
+            pd.testing.assert_frame_equal(cached.return_dataframe, fresh.return_dataframe)
+            self.assertFalse(
+                np.allclose(
+                    first.return_dataframe[LABEL], fresh.return_dataframe[LABEL], equal_nan=True
+                )
+            )
+
+    def test_root_target_configuration_isolates_models_and_predictions(self):
+        with temporary_directory() as directory:
+            implementations = directory / "implementations"
+            implementations.mkdir()
+            (implementations / "configured_targets.py").write_text(
+                "from QuickAdapterV3 import QuickAdapterV3\n"
+                "class ConfiguredTargets(QuickAdapterV3):\n"
+                "    def set_freqai_targets(self, dataframe, metadata, **kwargs):\n"
+                "        result = super().set_freqai_targets(dataframe, metadata, **kwargs)\n"
+                "        result['&s-extrema'] *= self.config['target_scale']\n"
+                "        return result\n"
+            )
+            frame = ohlcv_frame(
+                100 + 8 * np.sin(np.arange(720) / 6) + 2 * np.sin(np.arange(720) / 23)
+            )
+            options = {"strategy_name": "ConfiguredTargets", "strategy_path": implementations}
+            first = self._run_backtest(
+                directory / "shared", frame, config_overrides={"target_scale": 1.0}, **options
+            )
+            changed = self._run_backtest(
+                directory / "shared", frame, config_overrides={"target_scale": -1.0}, **options
+            )
+            fresh = self._run_backtest(
+                directory / "fresh", frame, config_overrides={"target_scale": -1.0}, **options
+            )
+            cached = self._run_backtest(
+                directory / "shared", frame, config_overrides={"target_scale": -1.0}, **options
+            )
+
+            self.assertNotEqual(first.data_path.parent, changed.data_path.parent)
+            self.assertEqual(changed.data_path.parent, cached.data_path.parent)
+            pd.testing.assert_frame_equal(changed.return_dataframe, fresh.return_dataframe)
+            pd.testing.assert_frame_equal(cached.return_dataframe, fresh.return_dataframe)
+            self.assertFalse(
+                np.allclose(
+                    first.return_dataframe[LABEL], fresh.return_dataframe[LABEL], equal_nan=True
+                )
+            )
+
+    def test_native_target_parameters_isolate_cache_by_resolved_values(self):
+        with temporary_directory() as directory:
+            implementations = directory / "implementations"
+            implementations.mkdir()
+            source = implementations / "parameter_targets.py"
+            source.write_text(
+                "from freqtrade.strategy import IntParameter\n"
+                "from QuickAdapterV3 import QuickAdapterV3\n"
+                "class ParameterTargets(QuickAdapterV3):\n"
+                "    target_scale = IntParameter(-1, 1, default=1, space='buy')\n"
+                "    ignored_scale = IntParameter(-1, 1, default=1, space='buy', load=False)\n"
+                "    def bot_start(self, **kwargs):\n"
+                "        super().bot_start(**kwargs)\n"
+                "        self.ft_load_hyper_params(False)\n"
+                "    def set_freqai_targets(self, dataframe, metadata, **kwargs):\n"
+                "        result = super().set_freqai_targets(dataframe, metadata, **kwargs)\n"
+                "        result['&s-extrema'] *= self.target_scale.value * self.ignored_scale.value\n"
+                "        return result\n"
+            )
+            frame = ohlcv_frame(
+                100 + 8 * np.sin(np.arange(720) / 6) + 2 * np.sin(np.arange(720) / 23)
+            )
+            options = {
+                "strategy_name": "ParameterTargets",
+                "strategy_path": implementations,
+                "config_overrides": {"trading_mode": "spot"},
+            }
+
+            def parameters(**values):
+                source.with_suffix(".json").write_text(
+                    json.dumps({"strategy_name": "ParameterTargets", "params": {"buy": values}})
+                )
+
+            parameters(target_scale=1, ignored_scale=-1)
+            first = self._run_backtest(directory / "shared", frame, **options)
+            parameters(ignored_scale=0)
+            default = self._run_backtest(directory / "shared", frame, **options)
+            self.assertEqual(first.data_path.parent, default.data_path.parent)
+            pd.testing.assert_frame_equal(first.return_dataframe, default.return_dataframe)
+
+            parameters(target_scale=-1, ignored_scale=-1)
+            changed = self._run_backtest(directory / "shared", frame, **options)
+            fresh = self._run_backtest(directory / "fresh", frame, **options)
+            parameters(target_scale=-1, ignored_scale=0)
+            cached = self._run_backtest(directory / "shared", frame, **options)
+
+            self.assertNotEqual(first.data_path.parent, changed.data_path.parent)
+            self.assertEqual(changed.data_path.parent, cached.data_path.parent)
+            pd.testing.assert_frame_equal(changed.return_dataframe, fresh.return_dataframe)
+            pd.testing.assert_frame_equal(cached.return_dataframe, fresh.return_dataframe)
+            self.assertFalse(
+                np.allclose(
+                    first.return_dataframe[LABEL], fresh.return_dataframe[LABEL], equal_nan=True
+                )
+            )
+
+    def test_typed_target_inputs_cannot_reuse_lossy_prediction_identities(self):
+        with temporary_directory() as directory:
+            implementations = directory / "implementations"
+            implementations.mkdir()
+            (implementations / "sentiment_strategy.py").write_text(
+                "import pandas as pd\n"
+                "from QuickAdapterV3 import QuickAdapterV3\n"
+                "class SentimentStrategy(QuickAdapterV3):\n"
+                "    def set_freqai_targets(self, dataframe, metadata, **kwargs):\n"
+                "        result = super().set_freqai_targets(dataframe, metadata, **kwargs)\n"
+                "        sentiment = result['sentiment']\n"
+                "        if isinstance(sentiment.dtype, pd.CategoricalDtype):\n"
+                "            direction = 2.0 * sentiment.cat.codes - 1.0\n"
+                "        else:\n"
+                "            direction = sentiment.map(lambda value: 1.0 if isinstance(value, int) else -1.0)\n"
+                "        result['&s-extrema'] *= direction\n"
+                "        return result\n"
+            )
+            options = {"strategy_name": "SentimentStrategy", "strategy_path": implementations}
+            for kind in ("object", "category"):
+                with self.subTest(kind=kind):
+                    frame = ohlcv_frame(
+                        100 + 8 * np.sin(np.arange(720) / 6) + 2 * np.sin(np.arange(720) / 23)
+                    )
+                    values = [1 if index % 2 == 0 else "neutral" for index in range(len(frame))]
+                    frame["sentiment"] = pd.Series(values, dtype=object)
+                    changed = frame.copy()
+                    if kind == "object":
+                        changed["sentiment"] = frame["sentiment"].map(str).astype(object)
+                    else:
+                        frame["sentiment"] = pd.Categorical(
+                            values, categories=[1, "neutral"], ordered=True
+                        )
+                        changed["sentiment"] = frame["sentiment"].cat.reorder_categories(
+                            ["neutral", 1]
+                        )
+
+                    first = self._run_backtest(directory / kind / "shared", frame, **options)
+                    reused = self._run_backtest(directory / kind / "shared", changed, **options)
+                    fresh = self._run_backtest(directory / kind / "fresh", changed, **options)
+                    repeated = self._run_backtest(directory / kind / "shared", changed, **options)
+
+                    self.assertNotEqual(first.data_path.parent, reused.data_path.parent)
+                    pd.testing.assert_frame_equal(reused.return_dataframe, fresh.return_dataframe)
+                    self.assertNotEqual(reused.data_path.parent, repeated.data_path.parent)
+                    pd.testing.assert_frame_equal(repeated.return_dataframe, fresh.return_dataframe)
+                    self.assertFalse(
+                        np.allclose(
+                            first.return_dataframe[LABEL],
+                            fresh.return_dataframe[LABEL],
+                            equal_nan=True,
+                        )
+                    )
+
+    def test_native_orderflow_structures_use_isolated_backtest_namespaces(self):
+        with temporary_directory() as directory:
+            frame = ohlcv_frame(
+                100 + 8 * np.sin(np.arange(720) / 6) + 2 * np.sin(np.arange(720) / 23)
+            )
+            frame["orderflow"] = [{100.0: {"bid": 1.0, "ask": 2.0}} for _ in range(len(frame))]
+
+            first = self._run_backtest(directory, frame)
+            second = self._run_backtest(directory, frame)
+
+            self.assertNotEqual(first.data_path.parent, second.data_path.parent)
+            pd.testing.assert_frame_equal(first.return_dataframe, second.return_dataframe)
+
+    def test_native_class_identity_isolates_cached_predictions_and_model_reload(self):
+        with temporary_directory() as directory:
+            implementations = directory / "implementations"
+            implementations.mkdir()
+            (implementations / "identity_strategies.py").write_text(
+                "from QuickAdapterV3 import QuickAdapterV3\n"
+                "class SharedStrategy(QuickAdapterV3):\n"
+                "    pass\n"
+                "class CacheStrategyA(SharedStrategy):\n"
+                "    pass\n"
+                "class CacheStrategyB(SharedStrategy):\n"
+                "    def set_freqai_targets(self, dataframe, metadata, **kwargs):\n"
+                "        result = super().set_freqai_targets(dataframe, metadata, **kwargs)\n"
+                "        result['&s-extrema'] = -result['&s-extrema']\n"
+                "        return result\n"
+            )
+            (implementations / "identity_models.py").write_text(
+                "from quickadapter.user_data.freqaimodels.QuickAdapterRegressorV3 import QuickAdapterRegressorV3\n"
+                "class CacheRegressorA(QuickAdapterRegressorV3):\n"
+                "    pass\n"
+                "class CacheRegressorB(CacheRegressorA):\n"
+                "    def predict(self, unfiltered_df, dk, **kwargs):\n"
+                "        result, status = super().predict(unfiltered_df, dk, **kwargs)\n"
+                "        result['&s-extrema'] = -result['&s-extrema']\n"
+                "        return result, status\n"
+            )
+            frame = ohlcv_frame(100.0 + np.sin(np.arange(720) / 9.0) * 4.0)
+            names = {"strategy_path": implementations, "model_path": implementations}
+            baseline = self._run_backtest(
+                directory / "shared",
+                frame,
+                strategy_name="CacheStrategyA",
+                model_name="CacheRegressorA",
+                **names,
+            )
+            for strategy_name, model_name in (
+                ("CacheStrategyB", "CacheRegressorA"),
+                ("CacheStrategyA", "CacheRegressorB"),
+            ):
+                with self.subTest(strategy=strategy_name, model=model_name):
+                    options = dict(names, strategy_name=strategy_name, model_name=model_name)
+                    cached = self._run_backtest(directory / "shared", frame, **options)
+                    fresh = self._run_backtest(
+                        directory / model_name / strategy_name, frame, **options
+                    )
+                    self.assertNotEqual(cached.data_path.parent, baseline.data_path.parent)
+                    pd.testing.assert_frame_equal(cached.return_dataframe, fresh.return_dataframe)
+                    baseline_values = baseline.return_dataframe[LABEL].dropna().to_numpy()
+                    cached_values = cached.return_dataframe[LABEL].dropna().to_numpy()
+                    self.assertFalse(np.array_equal(baseline_values, cached_values))
+                    if model_name == "CacheRegressorB":
+                        np.testing.assert_allclose(cached_values, -baseline_values)
+
+                    path = cached.backtesting_results_path
+                    damaged = pd.read_feather(path).drop(columns=[_PRODUCED_PREDICTION_COLUMN])
+                    damaged.to_feather(path)
+                    resumed = self._run_backtest(directory / "shared", frame, **options)
+                    self.assertEqual(resumed.data_path.parent, cached.data_path.parent)
+                    pd.testing.assert_frame_equal(resumed.return_dataframe, fresh.return_dataframe)
+
+    def test_inherited_source_change_invalidates_native_strategy_cache(self):
+        with temporary_directory() as directory:
+            implementations = directory / "implementations"
+            implementations.mkdir()
+            base_name = f"_qa_cache_base_{directory.name.replace('-', '_')}"
+            self.addCleanup(sys.modules.pop, base_name, None)
+            base_path = implementations / f"{base_name}.py"
+            base_source = (
+                "from QuickAdapterV3 import QuickAdapterV3\n"
+                "class CacheBaseStrategy(QuickAdapterV3):\n"
+                "    target_scale = 1.0\n"
+                "    def set_freqai_targets(self, dataframe, metadata, **kwargs):\n"
+                "        result = super().set_freqai_targets(dataframe, metadata, **kwargs)\n"
+                "        result['&s-extrema'] *= self.target_scale\n"
+                "        return result\n"
+            )
+            base_path.write_text(base_source)
+            (implementations / "inherited_strategy.py").write_text(
+                f"from {base_name} import CacheBaseStrategy\n"
+                "class InheritedCacheStrategy(CacheBaseStrategy):\n"
+                "    pass\n"
+            )
+            frame = ohlcv_frame(100.0 + np.sin(np.arange(720) / 9.0) * 4.0)
+            options = {"strategy_name": "InheritedCacheStrategy", "strategy_path": implementations}
+            first = self._run_backtest(directory / "shared", frame, **options)
+
+            base_path.write_text(base_source.replace("target_scale = 1.0", "target_scale = -1.0"))
+            sys.modules.pop(base_name)
+            importlib.invalidate_caches()
+            changed = self._run_backtest(directory / "shared", frame, **options)
+            fresh = self._run_backtest(directory / "fresh", frame, **options)
+
+            self.assertNotEqual(first.data_path.parent, changed.data_path.parent)
+            pd.testing.assert_frame_equal(changed.return_dataframe, fresh.return_dataframe)
+            self.assertFalse(
+                np.array_equal(
+                    first.return_dataframe[LABEL].dropna().to_numpy(),
+                    changed.return_dataframe[LABEL].dropna().to_numpy(),
+                )
+            )
+
+    def test_shifted_cache_is_recomputed_and_changed_inputs_isolate_models(self):
+        with temporary_directory() as directory:
+            position = np.arange(720)
+            frame = ohlcv_frame(100 + 8 * np.sin(position / 6) + 2 * np.sin(position / 23))
+            frame["date"] = pd.to_datetime(frame["date"], utc=True)
+            original = self._run_backtest(directory, frame)
+            result = original.return_dataframe
+            available = result["date"].between(
+                pd.Timestamp("2024-01-02", tz="UTC"),
+                pd.Timestamp("2024-01-03", tz="UTC"),
+                inclusive="left",
+            )
+            np.testing.assert_array_equal(result.loc[available, "prediction_available"], 1)
+            self.assertTrue(result.loc[~available, LABEL].isna().all())
+            for column in ("extrema_direction_smoothed", "extrema_weight_smoothed"):
+                self.assertTrue(np.isfinite(result.loc[available, column]).all())
+                self.assertNotIn(column, original.training_features_list)
+            for damage in ("dates", LABEL, "do_predict", "extrema_direction_smoothed"):
+                with self.subTest(cache_damage=damage):
+                    cached = pd.read_feather(original.backtesting_results_path)
+                    if damage == "dates":
+                        cached["date"] += pd.Timedelta(minutes=5)
+                    else:
+                        cached = cached.drop(columns=[damage])
+                    cached.to_feather(original.backtesting_results_path)
+
+                    restored = self._run_backtest(directory, frame)
+
+                    pd.testing.assert_series_equal(restored.return_dataframe[LABEL], result[LABEL])
+                    np.testing.assert_array_equal(
+                        restored.return_dataframe.loc[available, "prediction_available"], 1
+                    )
+                    np.testing.assert_array_equal(
+                        restored.return_dataframe.loc[available, "do_predict"], 1
+                    )
+            changed = frame.copy()
+            changed["volume"] = 10 + np.sin(position / 3)
+            different = self._run_backtest(directory, changed)
+            self.assertNotEqual(original.data_path.parent, different.data_path.parent)
+            self.assertTrue(
+                (original.data_path / f"{original.model_filename}_model.joblib").is_file()
+            )
+            self.assertTrue(
+                (different.data_path / f"{different.model_filename}_model.joblib").is_file()
+            )
+
+
+if __name__ == "__main__":
+    unittest.main()
index e99c76210042877826867b99143533fd051d7553..2c2b9db40fdf12984030be392275337f05b1d163 100644 (file)
@@ -169,10 +169,6 @@ class RegressorPredictionDedupTest(QaTestCase):
         with self.assertLogs(regressor.logger, level="WARNING") as captured:
             result = _dedupe_historic_predictions_on_date_pred(frame, PAIR)
         self.assertEqual(len(captured.records), 1)
-        self.assertEqual(
-            captured.records[0].getMessage(),
-            f"FreqAI prediction history [{PAIR}]: discarded invalid date_pred entries (count=2)",
-        )
         self.assertEqual(result["date_pred"].tolist(), list(DATES[:3]))
         self.assertEqual(result["value"].tolist(), [1.0, 2.0, 3.0])
 
@@ -225,7 +221,9 @@ class RegressorPredictionDedupTest(QaTestCase):
         result = _ensure_produced_prediction_column(without)
         self.assertIsNot(result, without)
         self.assertNotIn(MARKER, without.columns)
-        self.assertEqual(result[MARKER].tolist(), [True, False, False, False])
+        self.assertEqual(result[MARKER].isna().tolist(), [False, True, False, True])
+        self.assertEqual(_produced_prediction_mask(result).tolist(), [True, False, False, False])
+        self.assertEqual(result[MARKER].dropna().tolist(), [True, False])
 
         partial = pd.DataFrame(
             {
@@ -235,9 +233,9 @@ class RegressorPredictionDedupTest(QaTestCase):
             }
         )
         filled = _ensure_produced_prediction_column(partial)
-        self.assertIsNot(filled, partial)
+        self.assertIs(filled, partial)
         self.assertEqual(partial[MARKER].isna().tolist(), [False, True, False, True])
-        self.assertEqual(filled[MARKER].tolist(), [True, False, True, False])
+        self.assertEqual(_produced_prediction_mask(filled).tolist(), [True, False, False, False])
 
         complete = pd.DataFrame(
             {
@@ -271,6 +269,7 @@ class RegressorPredictionDedupTest(QaTestCase):
             aligned["do_predict"],
             pd.Series([1.0, 0.0, 1.0, 0.0], index=strategy.index, name="do_predict"),
         )
+        self.assertEqual(aligned[MARKER].tolist(), [True, False, True, False])
 
         drawer = drawer_stub(history)
         merged = FreqaiDataDrawer.attach_return_values_to_return_dataframe(drawer, PAIR, strategy)
@@ -288,6 +287,7 @@ class RegressorPredictionDedupTest(QaTestCase):
         aligned = _align_historic_predictions(history, strategy)
         self.assertEqual(aligned["do_predict"].tolist(), [0, 0, 0, 0])
         self.assertEqual(aligned["value"].isna().tolist(), [True, False, True, True])
+        self.assertNotIn(MARKER, aligned)
 
     def test_align_rejects_a_duplicate_date_pred(self):
         duplicated = pd.DataFrame(
index 57142626855a4accfde59709b0a227c02f046d10..656906b2d576d7a5a9e7a63ad9daeae15c7789c1 100644 (file)
@@ -40,6 +40,10 @@ def regressor(**attrs: object) -> QuickAdapterRegressorV3:
     model._optuna_hp_params = {}
     model._holdout_rmse = {}
     model._session_fitted_pairs = {PAIR}
+    model._metric_model_identity = {}
+    model._loaded_holdout_rmse = {}
+    model._backtest_calibration_starts = {}
+    model._backtest_current_metrics = {}
     model._optuna_hyperopt = False
     model.ft_params = {"label_period_candles": 1}
     model._label_defaults = (1, 5.0)
@@ -474,59 +478,6 @@ class RegressorPredictionHistoryTest(QaTestCase):
             }
         )
 
-    def test_marker_is_set_on_the_current_candle_and_its_return_mirror_only(self):
-        model, dd = self._model_with_appended_history(
-            ["2026-01-01 00:00", "2026-01-01 00:05", "2026-01-01 00:10"]
-        )
-        dk = kitchen()
-        dk.return_dataframe = dd.historic_predictions[PAIR].copy()
-
-        def native_overwrite(self, dataframe, dk, pair, trained_timestamp):
-            current = pd.Timestamp(dataframe["date"].iloc[-1])
-            frame = dd.historic_predictions[pair]
-            frame.at[frame.index[-1], "date_pred"] = current
-            dd.model_return_values[pair] = frame.tail(1).reset_index(drop=True)
-            dk.return_dataframe = frame.tail(1).reset_index(drop=True)
-
-        with mock.patch.object(
-            IFreqaiModel,
-            "build_strategy_return_arrays",
-            autospec=True,
-            side_effect=native_overwrite,
-        ):
-            model.build_strategy_return_arrays(self._strategy_frame(), dk, PAIR, 0)
-
-        self.assertEqual(
-            dd.historic_predictions[PAIR][_PRODUCED_PREDICTION_COLUMN].tolist(),
-            [False, False, True],
-        )
-        self.assertEqual(dd.model_return_values[PAIR][_PRODUCED_PREDICTION_COLUMN].tolist(), [True])
-        self.assertNotIn(_PRODUCED_PREDICTION_COLUMN, dk.return_dataframe)
-        self.assertEqual(model._calibration_current_candles, {})
-
-    def test_no_marker_when_the_saved_candle_predates_the_strategy_frame(self):
-        model, dd = self._model_with_appended_history(
-            ["2026-01-01 00:50", "2026-01-01 00:55", "2026-01-01 01:00"]
-        )
-        dk = kitchen()
-        dk.return_dataframe = dd.historic_predictions[PAIR].copy()
-
-        def stale_append(self, dataframe, dk, pair, trained_timestamp):
-            dk.return_dataframe = dd.historic_predictions[pair].tail(1).reset_index(drop=True)
-
-        with mock.patch.object(
-            IFreqaiModel, "build_strategy_return_arrays", autospec=True, side_effect=stale_append
-        ):
-            model.build_strategy_return_arrays(self._strategy_frame(), dk, PAIR, 0)
-
-        self.assertEqual(
-            dd.historic_predictions[PAIR][_PRODUCED_PREDICTION_COLUMN].tolist(), [False] * 3
-        )
-        self.assertEqual(
-            dd.model_return_values[PAIR][_PRODUCED_PREDICTION_COLUMN].tolist(), [False]
-        )
-        self.assertNotIn(_PRODUCED_PREDICTION_COLUMN, dk.return_dataframe)
-
     def test_the_current_candle_is_released_even_when_the_native_append_fails(self):
         model, dd = self._model_with_appended_history(
             ["2026-01-01 00:00", "2026-01-01 00:05", "2026-01-01 00:10"]
@@ -550,51 +501,6 @@ class RegressorPredictionHistoryTest(QaTestCase):
             dd.historic_predictions[PAIR][_PRODUCED_PREDICTION_COLUMN].tolist(), [False] * 3
         )
 
-    def test_return_dataframe_is_always_stripped_of_the_private_column(self):
-        model, _ = self._model_with_appended_history(
-            ["2026-01-01 00:00", "2026-01-01 00:05", "2026-01-01 00:10"]
-        )
-        dk = kitchen()
-        dk.return_dataframe = pd.DataFrame({_PRODUCED_PREDICTION_COLUMN: [True], LABEL: [0.0]})
-
-        with mock.patch.object(IFreqaiModel, "build_strategy_return_arrays", autospec=True):
-            model.build_strategy_return_arrays(self._strategy_frame(), dk, PAIR, 0)
-
-        self.assertNotIn(_PRODUCED_PREDICTION_COLUMN, dk.return_dataframe)
-        self.assertIn(LABEL, dk.return_dataframe)
-
-    def test_bootstrap_history_gains_an_all_false_produced_column(self):
-        frame = history(["2026-01-01 00:00", "2026-01-01 00:05"], [False, False])
-        dd = drawer()
-        dd.historic_predictions[PAIR] = frame
-        model = regressor(dd=dd)
-        strat_df = pd.DataFrame(
-            {
-                "date": pd.to_datetime(["2026-01-01 00:00", "2026-01-01 00:05"], utc=True),
-                "high": [101.0, 101.0],
-                "low": [99.0, 99.0],
-                "close": [100.0, 100.0],
-            }
-        )
-        bootstrap = pd.DataFrame({LABEL: [0.0, 0.0]})
-        dk = kitchen()
-        dk.data["extra_returns_per_train"] = {}
-
-        with mock.patch.object(
-            IFreqaiModel,
-            "set_initial_historic_predictions",
-            autospec=True,
-            side_effect=lambda self, *args: self.dd.historic_predictions.__setitem__(
-                PAIR, bootstrap.copy()
-            ),
-        ):
-            model.set_initial_historic_predictions(bootstrap, dk, PAIR, strat_df)
-
-        self.assertEqual(
-            dd.historic_predictions[PAIR][_PRODUCED_PREDICTION_COLUMN].tolist(),
-            [False, False],
-        )
-
     # ---------------------------------------------------------------- DI cutoff sentinels
 
     def test_cold_pair_uses_sentinels_outside_the_normalized_label_range(self):
@@ -734,7 +640,7 @@ class RegressorPredictionHistoryTest(QaTestCase):
             ],
             [True, True, True, False, True, True],
             values=[-0.5, 0.5, 0.0, 5.0, 9.0, 7.0],
-            holdout=[0.25, 0.5, np.nan, 888.0, 999.0, 777.0],
+            holdout=[0.25, 0.5, 0.75, 888.0, 999.0, 777.0],
         )
         persisted.loc[5, "date_pred"] = pd.NaT
         for order in (list(range(6)), [4, 2, 0, 3, 1, 5]):
@@ -750,11 +656,11 @@ class RegressorPredictionHistoryTest(QaTestCase):
                 model._holdout_rmse = {PAIR: float("inf")}
                 model._session_fitted_pairs = set()
                 model.fit_live_predictions(dk, PAIR)
-                self.assertEqual(dk.data["extra_returns_per_train"]["holdout_rmse"], 0.5)
+                self.assertEqual(dk.data["extra_returns_per_train"]["holdout_rmse"], 0.75)
                 self.assertEqual(dk.data["labels_mean"][LABEL], 0.0)
 
     def test_unavailable_replayed_holdout_does_not_resurrect_an_older_score(self):
-        for latest, expected in ((np.inf, np.inf), (np.nan, 0.5)):
+        for latest, expected in ((np.inf, np.inf), (np.nan, np.inf), (-1, np.inf), (0, 0)):
             with self.subTest(latest=latest):
                 model, dk, _ = calibrated(candles=2, holdout=[0.5, latest])
                 model._holdout_rmse = {PAIR: np.inf}
@@ -772,6 +678,55 @@ class RegressorPredictionHistoryTest(QaTestCase):
 
         self.assertTrue(np.isposinf(extra["holdout_rmse"]))
 
+    def _hpo_window(self):
+        frame = pd.DataFrame(
+            {
+                "date": pd.date_range("2026-01-01", periods=12, freq=CANDLE_FREQ, tz="UTC"),
+                "close": np.arange(12, dtype=float),
+            }
+        )
+        model = regressor(live=False, dd=drawer())
+        model.data_provider = SimpleNamespace(get_pair_dataframe=lambda **kwargs: frame)
+        model.dd.historic_predictions[PAIR] = frame.iloc[:2]
+        dk = kitchen()
+        dk.full_df = frame
+        return model, dk, frame
+
+    def test_label_hpo_uses_current_cursor_after_missing_predictions(self):
+        for observations in (0, 2):
+            with self.subTest(produced_observations=observations):
+                model, dk, frame = self._hpo_window()
+                model.dd.historic_predictions[PAIR] = frame.iloc[:observations]
+                model._calibration_current_candles[PAIR] = frame["date"].iloc[7]
+
+                result = model._label_hpo_dataframe_as_of_prediction_time(dk, PAIR)
+
+                pd.testing.assert_frame_equal(result, frame.iloc[:8])
+
+    def test_label_hpo_keeps_historical_inference_without_a_cursor(self):
+        model, dk, frame = self._hpo_window()
+
+        result = model._label_hpo_dataframe_as_of_prediction_time(dk, PAIR)
+
+        pd.testing.assert_frame_equal(result, frame.iloc[:3])
+
+    def test_label_hpo_rejects_an_unknown_explicit_cursor(self):
+        model, dk, _ = self._hpo_window()
+        model._calibration_current_candles[PAIR] = pd.NaT
+
+        result = model._label_hpo_dataframe_as_of_prediction_time(dk, PAIR)
+
+        self.assertTrue(result.empty)
+
+    def test_label_hpo_live_uses_the_provider_window(self):
+        model, dk, frame = self._hpo_window()
+        model.live = True
+        model._calibration_current_candles[PAIR] = frame["date"].iloc[3]
+
+        result = model._label_hpo_dataframe_as_of_prediction_time(dk, PAIR)
+
+        pd.testing.assert_frame_equal(result, frame)
+
     # --------------------------------------------------------------- optuna throttling
 
     def _throttled(self, budget: int, pool: list[int] | None = None) -> QuickAdapterRegressorV3:
index 5bfea671b615c16010feb35946950c92ea0e0c26..ea30997b54196cff41a143cd9e18673c99595b6c 100644 (file)
@@ -33,6 +33,35 @@ def state(**overrides):
     return base
 
 
+class LeverageTest(QaTestCase):
+    def _leverage(self, strategy, proposal=3.0, maximum=10.0):
+        return strategy.leverage("BTC/USDT", CANDLE, 100.0, proposal, maximum, None, "long")
+
+    def test_unrepresentable_integer_warns_and_keeps_proposal_fallback_per_call(self):
+        strategy = QuickAdapterV3.__new__(QuickAdapterV3)
+        strategy.config = {"leverage": 10**500}
+        with self.assertLogs("QuickAdapterV3", level="WARNING"):
+            self.assertEqual(self._leverage(strategy), 3.0)
+        self.assertEqual(self._leverage(strategy, proposal=7.0, maximum=5.0), 5.0)
+
+    def test_configured_values_preserve_numeric_only_validation_and_bounds(self):
+        for value, expected in (
+            (2, 2.0),
+            (0.5, 1.0),
+            (20.0, 10.0),
+            (None, 3.0),
+            ("2", 3.0),
+            (True, 3.0),
+            (np.nan, 3.0),
+            (np.inf, 3.0),
+            (-np.inf, 3.0),
+        ):
+            with self.subTest(value=value):
+                strategy = QuickAdapterV3.__new__(QuickAdapterV3)
+                strategy.config = {"leverage": value}
+                self.assertEqual(self._leverage(strategy), expected)
+
+
 class CandleDateTest(QaTestCase):
     def test_an_iso_string_is_parsed_as_utc(self):
         self.assertEqual(QuickAdapterV3._as_utc_candle_date("2026-01-01T12:00:00+00:00"), CANDLE)
index 4e7a020fbe25ce09b498d3a30419ca0c34ac84e8..1aa9129a3aea91f433a66018c278189ae46f3928 100644 (file)
@@ -194,6 +194,7 @@ class RuntimeSizingTest(QaTestCase):
         config = model_config(
             directory,
             runmode=RunMode.BACKTEST,
+            trading_mode="futures",
             exit_pricing={
                 "trade_natr_method": "quantile_interpolation",
                 "final_take_profit_retracement_fraction": 0.25,
@@ -227,6 +228,12 @@ class RuntimeSizingTest(QaTestCase):
             ),
         )
 
+    def test_sizing_fixture_requests_backtest_and_futures_explicitly(self):
+        self.assertEqual(self.model.config["runmode"], RunMode.BACKTEST)
+        self.assertEqual(self.model.config["trading_mode"], "futures")
+        self.assertFalse(self.model.is_trade_runmode)
+        self.assertTrue(self.model.can_short)
+
     def position(self, short=False, exit_stage=0):
         position = Trade(
             pair=PAIR,
index 4cf50543b3488308165302f504442869f4dd89ef..3274d79b282778273d438c3681f4a3ae6fdfcbfd 100644 (file)
@@ -2,9 +2,14 @@
 
 import importlib
 import sys
+import unittest
 from pathlib import Path
+from types import ModuleType
+from unittest import mock
 
-from qa_support import REPO_ROOT, QaTestCase
+from freqtrade.enums import RunMode
+from qa_support import REPO_ROOT, QaTestCase, model_config, temporary_directory
+from QuickAdapterV3 import QuickAdapterV3
 
 QUICKADAPTER = REPO_ROOT / "quickadapter"
 MEASURED_SOURCE = QUICKADAPTER / "user_data"
@@ -20,8 +25,7 @@ PRODUCTION_NAMES = tuple(
     else src.relative_to(QUICKADAPTER.parent).with_suffix("").as_posix().replace("/", ".")
     for src in _SOURCES
 )
-# A dotted strategies import is banned, so neither branch below may ever build one.
-_FORBIDDEN_PREFIXES = ("quickadapter.user_data.strategies.", "quickadapter.tests.")
+TESTS_ROOT = QUICKADAPTER / "tests"
 
 # Imported here, at module scope, so the parity contract holds on its own: unittest discovery
 # imports every test module before running any test, so a lazy import inside a method would make
@@ -30,16 +34,26 @@ for _name in PRODUCTION_NAMES:
     importlib.import_module(_name)
 
 
+def _source_files_under(root: Path) -> list[tuple[str, Path]]:
+    """Return module registrations by resolved file, independently of import spelling."""
+    found = []
+    for name, module in list(sys.modules.items()):
+        raw = getattr(module, "__file__", None)
+        if raw:
+            path = Path(raw).resolve()
+            if path.suffix == ".py" and path.is_relative_to(root.resolve()):
+                found.append((name, path))
+    return sorted(found)
+
+
 class SuiteContractTest(QaTestCase):
     def test_a_every_source_file_has_a_loaded_module(self):
-        loaded = {
-            Path(module.__file__).resolve()
-            for module in list(sys.modules.values())
-            if getattr(module, "__file__", None)
-        }
-        for src in _SOURCES:
+        registered = _source_files_under(MEASURED_SOURCE)
+        for src, expected in zip(_SOURCES, PRODUCTION_NAMES, strict=True):
             with self.subTest(module=src.name):
-                self.assertIn(src.resolve(), loaded)
+                self.assertEqual(
+                    [expected], [name for name, path in registered if path == src.resolve()]
+                )
 
     def test_no_package_marker_under_quickadapter(self):
         markers = sorted(
@@ -53,10 +67,67 @@ class SuiteContractTest(QaTestCase):
         )
 
     def test_no_module_is_registered_under_a_second_name(self):
-        found = sorted(name for name in sys.modules if name.startswith(_FORBIDDEN_PREFIXES))
-        self.assertEqual(
-            [],
-            found,
-            "a shadow copy carries its own _LABEL_GENERATORS and _WARNED_CONFIG_DEPRECATIONS, so "
-            "every reset in qa_support would clean a module the code under test never touches",
-        )
+        self.test_a_every_source_file_has_a_loaded_module()
+
+    def test_every_test_module_is_registered_under_its_bare_stem(self):
+        for name, path in _source_files_under(TESTS_ROOT):
+            with self.subTest(module=name):
+                self.assertEqual(path.stem, name)
+
+
+class SuiteContractRegressionTest(QaTestCase):
+    def test_actual_guard_rejects_aliases_by_file_not_prefix(self):
+        for stem, alias in (
+            ("QuickAdapterRegressorV3", "QuickAdapterRegressorV3"),
+            ("QuickAdapterRegressorV3", "arbitrary_production_alias"),
+            ("QuickAdapterV3", "arbitrary_strategy_alias"),
+        ):
+            with self.subTest(alias=alias):
+                source = next(src for src in _SOURCES if src.stem == stem)
+                module = ModuleType(alias)
+                module.__file__ = str(source)
+                result = unittest.TestResult()
+                with mock.patch.dict(sys.modules, {alias: module}):
+                    SuiteContractTest("test_a_every_source_file_has_a_loaded_module").run(result)
+                self.assertEqual(result.errors, [])
+                self.assertEqual(len(result.failures), 1)
+
+    def test_actual_guard_rejects_dotted_test_registration(self):
+        module = ModuleType("quickadapter.tests.shadow")
+        module.__file__ = __file__
+        result = unittest.TestResult()
+        with mock.patch.dict(sys.modules, {module.__name__: module}):
+            SuiteContractTest("test_every_test_module_is_registered_under_its_bare_stem").run(
+                result
+            )
+        self.assertEqual(result.errors, [])
+        self.assertEqual(len(result.failures), 1)
+
+
+class ConfigBuilderTest(QaTestCase):
+    def test_defaults_and_explicit_overrides_are_private_and_preserved(self):
+        with temporary_directory() as directory:
+            default = model_config(str(directory))
+            self.assertEqual(default["runmode"], RunMode.DRY_RUN)
+            self.assertEqual(default["user_data_dir"], directory)
+            overrides = {"feature_parameters": {"label_period_candles": 7}}
+            config = model_config(
+                directory,
+                runmode=RunMode.BACKTEST,
+                user_data_dir=str(directory / "alternate"),
+                trading_mode="futures",
+                freqai=overrides,
+            )
+            self.assertEqual(config["runmode"], RunMode.BACKTEST)
+            self.assertEqual(config["user_data_dir"], directory / "alternate")
+            self.assertEqual(config["trading_mode"], "futures")
+            self.assertEqual(config["freqai"]["feature_parameters"]["include_timeframes"], ["5m"])
+            self.assertEqual(config["freqai"]["feature_parameters"]["label_period_candles"], 7)
+            strategy = QuickAdapterV3(config)
+            self.assertFalse(strategy.is_trade_runmode)
+            config["freqai"]["feature_parameters"]["label_period_candles"] = 99
+            config["exchange"]["pair_whitelist"].append("ETH/USDT")
+            self.assertEqual(overrides["feature_parameters"]["label_period_candles"], 7)
+            fresh = model_config(directory)
+            self.assertEqual(fresh["freqai"]["feature_parameters"]["label_period_candles"], 1)
+            self.assertEqual(len(fresh["exchange"]["pair_whitelist"]), 1)
index 9bc20e75e00ae9a2870f75dcc3c2b05866035d3d..9e08372e707d1b9a8773d9d53b91260a6348d623 100644 (file)
@@ -8,8 +8,11 @@ from collections.abc import Set as AbstractSet
 from dataclasses import dataclass
 from datetime import datetime, timezone
 from functools import cached_property, wraps
-from inspect import iscoroutinefunction, unwrap
+from hashlib import sha256
+from importlib.metadata import version as distribution_version
+from inspect import getfile, iscoroutinefunction, unwrap
 from pathlib import Path
+from types import MethodType
 from typing import (
     Any,
     ClassVar,
@@ -20,6 +23,7 @@ from typing import (
     cast,
     get_args,
 )
+from uuid import uuid4
 
 import numpy as np
 import optuna
@@ -41,6 +45,7 @@ from freqtrade.freqai.data_drawer import (
     FreqaiDataDrawer,
 )
 from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
+from freqtrade.strategy.interface import IStrategy
 
 # Disabled: scikit-learn-extra 0.3.0 fails on Python 3.14 (__gxx_personality_v0).
 # from sklearn_extra.cluster import KMedoids
@@ -82,6 +87,10 @@ from Utils import (
     DEFAULT_MIN_LABEL_PERIOD_CANDLES,
     DEFAULT_REGRESSOR,
     DEFAULTS_LABEL_PREDICTION,
+    EXTREMA_DIRECTION_COLUMN,
+    EXTREMA_DIRECTION_SMOOTHED_COLUMN,
+    EXTREMA_WEIGHT_COLUMN,
+    EXTREMA_WEIGHT_SMOOTHED_COLUMN,
     LABEL_COLUMNS,
     REGRESSORS,
     WEIGHT_STRATEGIES,
@@ -178,16 +187,21 @@ def _produced_prediction_mask(frame: pd.DataFrame) -> NDArray[np.bool_]:
 
 
 def _ensure_produced_prediction_column(frame: pd.DataFrame) -> pd.DataFrame:
+    """Infer proven legacy outputs without turning ambiguous zeros into placeholders."""
+    inferred = pd.Series(np.nan, index=frame.index, dtype=object)
+    inferred.loc[_legacy_produced_prediction_mask(frame)] = True
+    if "do_predict" in frame:
+        inferred.loc[pd.to_numeric(frame["do_predict"], errors="coerce").eq(2)] = False
     if _PRODUCED_PREDICTION_COLUMN not in frame:
         result = frame.copy()
-        result[_PRODUCED_PREDICTION_COLUMN] = _legacy_produced_prediction_mask(frame)
+        result[_PRODUCED_PREDICTION_COLUMN] = inferred
         return result
-    missing = frame[_PRODUCED_PREDICTION_COLUMN].isna()
+    missing = frame[_PRODUCED_PREDICTION_COLUMN].isna() & inferred.notna()
     if not missing.any():
         return frame
     result = frame.copy()
     result[_PRODUCED_PREDICTION_COLUMN] = frame[_PRODUCED_PREDICTION_COLUMN].where(
-        ~missing, _legacy_produced_prediction_mask(frame)
+        ~missing, inferred
     )
     return result
 
@@ -241,6 +255,10 @@ def _align_historic_predictions(history: pd.DataFrame, dataframe: pd.DataFrame)
         aligned["do_predict"] = aligned["do_predict"].where(~missing, 0)
     else:
         aligned["do_predict"] = 0
+    if _PRODUCED_PREDICTION_COLUMN in aligned:
+        aligned[_PRODUCED_PREDICTION_COLUMN] = aligned[_PRODUCED_PREDICTION_COLUMN].where(
+            ~missing, False
+        )
     return aligned
 
 
@@ -452,11 +470,18 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
     https://github.com/sponsors/robcaulk
     """
 
-    version = "3.13.0-rc.15"
+    version = "3.13.0-rc.16"
 
     _CALIBRATION_START_KEY: Final[str] = "quickadapter_calibration_start"
     _DEPLOYMENT_COORDINATE_MARKER_KEY: Final[str] = "quickadapter_deployment_coordinates"
     _DEPLOYMENT_COORDINATE_GENERATION: Final[str] = "frozen-pipelines-v1"
+    _BACKTEST_CONTEXT_KEY: Final[str] = "quickadapter_backtest_context"
+    _DIAGNOSTIC_COLUMNS: Final[tuple[str, ...]] = (
+        EXTREMA_DIRECTION_COLUMN,
+        EXTREMA_DIRECTION_SMOOTHED_COLUMN,
+        EXTREMA_WEIGHT_COLUMN,
+        EXTREMA_WEIGHT_SMOOTHED_COLUMN,
+    )
 
     _TEST_SIZE: Final[float] = 0.1
     _SKLEARN_TRAIN_TEST_SPLIT_KEYS: Final[frozenset[str]] = frozenset(
@@ -491,7 +516,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         ValueError,
         json.JSONDecodeError,
     )
-    _OPTUNA_JOURNAL_QUARANTINE_TIE_BREAK_LIMIT: Final[int] = 8
+    _OPTUNA_JOURNAL_QUARANTINE_TIE_BREAK_LIMIT: Final[int] = 99
     _OPTUNA_JOURNAL_OP_CODE_KEY: Final[str] = "op_code"
     _OPTUNA_JOURNAL_OPERATION_CODES: Final[frozenset[int]] = frozenset(range(10))
     _OPTUNA_JOURNAL_TAIL_PROBE_BYTES: Final[int] = 65536
@@ -760,14 +785,17 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         row's LOCAL position in ``unfiltered_df`` to recover the local
         index at which the label becomes causally available.
 
-        Row-wise ``max`` of every present ``<label>_known_at_lookahead`` and
-        ``<label>_weight_known_at_lookahead`` column (weight availability lags
-        the label by one pivot); columns missing or with any NaN are skipped
-        silently (opt-in by emission). Returns ``None`` when none is usable;
-        callers then fall back to the position-based purge.
+        Present channels contribute their row-wise maximum. Missing or invalid
+        values make only their row unavailable until the end of this frame.
+        Entirely absent channels retain the fixed-horizon fallback.
         """
         QuickAdapterRegressorV3._validate_index_alignment(filtered_dataframe, unfiltered_df)
-        series_list: list[pd.Series] = []
+        positions = QuickAdapterRegressorV3._row_positions(
+            filtered_dataframe, unfiltered_df
+        ).to_numpy(dtype=np.int64)
+        unknown = len(unfiltered_df) - positions
+        combined = np.zeros(len(filtered_dataframe), dtype=np.int64)
+        found = False
         for label_col in LABEL_COLUMNS:
             for lookahead_col in (
                 label_known_at_lookahead_column_name(label_col),
@@ -775,15 +803,23 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             ):
                 if lookahead_col not in unfiltered_df.columns:
                     continue
-                lookahead = unfiltered_df.loc[filtered_dataframe.index, lookahead_col]
-                if lookahead.isna().any():
-                    continue
-                series_list.append(pd.to_numeric(lookahead, errors="raise"))
-        if not series_list:
+                found = True
+                numeric = pd.to_numeric(
+                    unfiltered_df.loc[filtered_dataframe.index, lookahead_col], errors="coerce"
+                )
+                if pd.api.types.is_integer_dtype(numeric.dtype):
+                    valid = numeric.ge(0) & numeric.le(np.iinfo(np.int64).max)
+                    values = numeric.where(
+                        valid.fillna(False), pd.Series(unknown, index=numeric.index)
+                    ).to_numpy(dtype=np.int64)
+                else:
+                    raw = numeric.to_numpy(dtype=float, na_value=np.nan)
+                    valid = np.isfinite(raw) & (raw >= 0) & (raw < 2**63) & (raw == np.floor(raw))
+                    values = np.where(valid, raw, unknown).astype(np.int64)
+                np.maximum(combined, values, out=combined)
+        if not found:
             return None
-        if len(series_list) == 1:
-            return series_list[0].astype(np.int64)
-        return pd.concat(series_list, axis=1).max(axis=1).astype(np.int64)
+        return pd.Series(combined, index=filtered_dataframe.index, dtype=np.int64)
 
     @staticmethod
     def _filter_train_by_mask(
@@ -1680,6 +1716,10 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         self._optuna_hp_value: dict[str, float] = {}
         self._holdout_rmse: dict[str, float] = {}
         self._session_fitted_pairs: set[str] = set()
+        self._metric_model_identity: dict[str, tuple[str, str]] = {}
+        self._loaded_holdout_rmse: dict[str, float | None] = {}
+        self._backtest_calibration_starts: dict[str, str] = {}
+        self._backtest_current_metrics: dict[str, dict[str, Any]] = {}
         self._calibration_current_candles: dict[str, pd.Timestamp] = {}
         self._optuna_label_values: dict[str, list[float | int]] = {}
         self._optuna_hp_params: dict[str, dict[str, Any]] = {}
@@ -1733,6 +1773,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             )
             self.regressor = DEFAULT_REGRESSOR
             self.freqai_info["regressor"] = self.regressor
+        self._backtest_config = copy.deepcopy(self.freqai_info)
+        self._install_prediction_transport()
         self._log_model_configuration()
 
     def _log_model_configuration(self) -> None:
@@ -2077,6 +2119,507 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             self._optuna_label_candle_pool.extend(sorted(optuna_label_available_candles))
             self._optuna_label_shuffle_rng.shuffle(self._optuna_label_candle_pool)
 
+    @staticmethod
+    def _coerce_holdout_rmse(value: Any) -> float:
+        """Keep genuine zero error; negative, missing and non-finite scores are unavailable."""
+        if (
+            isinstance(value, (int, float, np.integer, np.floating))
+            and not isinstance(value, (bool, np.bool_))
+            and np.isfinite(value)
+            and value >= 0
+        ):
+            return float(value)
+        return np.inf
+
+    @staticmethod
+    def _kitchen_model_identity(dk: FreqaiDataKitchen) -> tuple[str, str]:
+        return str(getattr(dk, "data_path", "")), str(getattr(dk, "model_filename", ""))
+
+    def _capture_model_metric(self, dk: FreqaiDataKitchen, pair: str) -> None:
+        """Capture the model's metadata before calibration rewrites its returned extras."""
+        identity = self._kitchen_model_identity(dk)
+        if self._metric_model_identity.get(pair) == identity:
+            return
+        self._metric_model_identity[pair] = identity
+        self._session_fitted_pairs.discard(pair)
+        extras = dk.data.get("extra_returns_per_train", {})
+        self._loaded_holdout_rmse[pair] = (
+            self._coerce_holdout_rmse(extras["holdout_rmse"]) if "holdout_rmse" in extras else None
+        )
+
+    def _ensure_history_schema(self, dk: FreqaiDataKitchen, pair: str) -> None:
+        """Extend old histories without fabricating past observations or scores."""
+        if pair not in self.dd.historic_predictions:
+            return
+        history = _ensure_produced_prediction_column(self.dd.historic_predictions[pair])
+        columns = dict.fromkeys(dk.label_list + dk.unique_class_list, np.nan)
+        for label in dk.label_list + dk.unique_class_list:
+            columns[f"{label}_mean"] = np.nan
+            columns[f"{label}_std"] = np.nan
+        columns.update(dk.data.get("extra_returns_per_train", {}))
+        columns["holdout_rmse"] = np.nan
+        if self.ft_params.get("DI_threshold", 0) > 0:
+            columns["DI_values"] = np.nan
+        for column, value in columns.items():
+            if column not in history:
+                dtype = (
+                    float
+                    if isinstance(value, (int, float, np.number))
+                    and not isinstance(value, (bool, np.bool_))
+                    else object
+                )
+                history[column] = pd.Series(np.nan, index=history.index, dtype=dtype)
+        self.dd.historic_predictions[pair] = history
+
+    def _normalise_prediction_returns(
+        self, frame: pd.DataFrame, provenance: pd.Series | None = None
+    ) -> pd.DataFrame:
+        """Show known absences as gaps, preserving genuine and ambiguous legacy zeros."""
+        if provenance is None:
+            if _PRODUCED_PREDICTION_COLUMN in frame:
+                provenance = frame[_PRODUCED_PREDICTION_COLUMN]
+            else:
+                provenance = pd.Series(np.nan, index=frame.index, dtype=object)
+                provenance.loc[_legacy_produced_prediction_mask(frame)] = True
+        availability = pd.to_numeric(provenance, errors="coerce").astype(float)
+        availability.index = frame.index
+        if "do_predict" in frame:
+            availability.loc[pd.to_numeric(frame["do_predict"], errors="coerce").eq(2)] = 0.0
+        frame["prediction_available"] = availability
+        absent = availability.eq(0)
+        for column in frame.columns:
+            if column.startswith("&") or column in (
+                "holdout_rmse",
+                "DI_values",
+                "DI_cutoff",
+                "DI_value_param1",
+                "DI_value_param2",
+                "DI_value_param3",
+            ):
+                values = pd.to_numeric(frame[column], errors="coerce").astype(float)
+                frame[column] = values.mask(absent)
+        if "holdout_rmse" not in frame:
+            frame["holdout_rmse"] = np.nan
+        else:
+            scores = frame["holdout_rmse"]
+            frame["holdout_rmse"] = scores.where(np.isfinite(scores) & scores.ge(0))
+        return frame.drop(columns=[_PRODUCED_PREDICTION_COLUMN], errors="ignore")
+
+    def _install_prediction_transport(self) -> None:
+        """Repair only this QuickAdapter drawer, leaving the shared SDK patch unchanged."""
+        drawer = self.dd
+        original_initial = drawer.set_initial_return_values
+        original_append = drawer.append_model_predictions
+        original_null = drawer.return_null_values_to_strategy
+
+        @wraps(original_initial)
+        def initial(
+            current: FreqaiDataDrawer, pair: str, predictions: pd.DataFrame, frame: pd.DataFrame
+        ) -> None:
+            history = _dedupe_historic_predictions_on_date_pred(
+                _ensure_produced_prediction_column(current.historic_predictions[pair]), pair
+            )
+            current.historic_predictions[pair] = history
+            missing = history.set_index("date_pred").isna()
+            original_initial(pair, predictions, frame)
+            result = current.historic_predictions[pair]
+            old_missing = missing.reindex(pd.DatetimeIndex(result["date_pred"]), fill_value=False)
+            old_missing.index = result.index
+            result[missing.columns] = result[missing.columns].mask(old_missing)
+            added = ~result["date_pred"].isin(missing.index)
+            result.loc[added, _PRODUCED_PREDICTION_COLUMN] = False
+            current.model_return_values[pair] = _align_historic_predictions(result, frame)
+
+        @wraps(original_append)
+        def append(
+            current: FreqaiDataDrawer,
+            pair: str,
+            predictions: pd.DataFrame,
+            status: NDArray[np.int_],
+            dk: FreqaiDataKitchen,
+            frame: pd.DataFrame,
+        ) -> None:
+            self._ensure_history_schema(dk, pair)
+            old_dates = pd.DatetimeIndex(current.historic_predictions[pair]["date_pred"])
+            original_append(pair, predictions, status, dk, frame)
+            history = current.historic_predictions[pair]
+            added = ~history["date_pred"].isin(old_dates)
+            history.loc[added, _PRODUCED_PREDICTION_COLUMN] = False
+            if (
+                not frame.empty
+                and not history.empty
+                and history["date_pred"].iloc[-1]
+                == pd.to_datetime(frame["date"].iloc[-1], utc=True)
+            ):
+                produced = dk.__dict__.get("_quickadapter_prediction_produced")
+                history.at[history.index[-1], _PRODUCED_PREDICTION_COLUMN] = bool(
+                    status[-1] != 2 and (produced is None or produced[-1])
+                )
+            current.model_return_values[pair] = _align_historic_predictions(history, frame)
+
+        @wraps(original_null)
+        def null(current: FreqaiDataDrawer, frame: pd.DataFrame, dk: FreqaiDataKitchen) -> None:
+            original_null(frame, dk)
+            dk.return_dataframe = self._normalise_prediction_returns(
+                dk.return_dataframe, pd.Series(False, index=dk.return_dataframe.index)
+            )
+
+        drawer.__dict__["set_initial_return_values"] = MethodType(initial, drawer)
+        drawer.__dict__["append_model_predictions"] = MethodType(append, drawer)
+        drawer.__dict__["return_null_values_to_strategy"] = MethodType(null, drawer)
+
+    def start(self, dataframe: pd.DataFrame, metadata: dict, strategy: Any) -> pd.DataFrame:
+        result = super().start(dataframe, metadata, strategy)
+        if "prediction_available" not in result:
+            result = self._normalise_prediction_returns(result)
+        return result
+
+    def start_backtesting_from_historic_predictions(
+        self, dataframe: pd.DataFrame, metadata: dict, dk: FreqaiDataKitchen
+    ) -> FreqaiDataKitchen:
+        dk = super().start_backtesting_from_historic_predictions(dataframe, metadata, dk)
+        frame = _ensure_produced_prediction_column(dk.return_dataframe)
+        provenance = frame[_PRODUCED_PREDICTION_COLUMN].mask(frame["date_pred"].isna(), False)
+        dk.return_dataframe = self._normalise_prediction_returns(frame, provenance)
+        return dk
+
+    def _backtest_fingerprint(self, prepared: pd.DataFrame, pair: str, strategy: Any) -> str:
+        """Bind cached predictions and models to their actual inputs and implementation."""
+        digest = sha256()
+        configuration = {
+            "freqai": self._backtest_config,
+            "model_config": self.config,
+            "strategy_config": strategy.config,
+            "strategy_parameters": {
+                name: parameter.value for name, parameter in strategy.enumerate_parameters()
+            },
+            "pair": pair,
+            "labels": {label: strategy.get_label_params(pair, label) for label in LABEL_COLUMNS},
+            "versions": {
+                name: distribution_version(name)
+                for name in (
+                    "freqtrade",
+                    "datasieve",
+                    "numpy",
+                    "pandas",
+                    "scipy",
+                    "scikit-learn",
+                    "optuna",
+                    "optuna-integration",
+                    "xgboost",
+                    "lightgbm",
+                    "catboost",
+                    "ngboost",
+                )
+            },
+        }
+        sdk_classes = set(BaseRegressionModel.__mro__) | set(IStrategy.__mro__)
+        implementations = {
+            role: [cls for cls in leaf.__mro__ if cls not in sdk_classes]
+            for role, leaf in (("strategy", type(strategy)), ("model", type(self)))
+        }
+        configuration["implementations"] = {
+            role: [(cls.__module__, cls.__qualname__) for cls in classes]
+            for role, classes in implementations.items()
+        }
+        source_cache: dict[tuple[str, str | Path], bytes] = {}
+        sources: dict[tuple[str, bytes], None] = {}
+
+        def add_source(path: str | Path, snapshot: str | None = None) -> None:
+            key = ("snapshot", snapshot) if snapshot is not None else ("file", Path(path).resolve())
+            if key not in source_cache:
+                source_cache[key] = (
+                    snapshot.encode() if snapshot is not None else Path(path).read_bytes()
+                )
+            sources[(Path(path).name, source_cache[key])] = None
+
+        for classes in implementations.values():
+            leaf = classes[0]
+            for cls in classes:
+                snapshot = cls.__dict__.get("__source__")
+                path = cls.__dict__.get("__file__")
+                if snapshot is None and path is None and cls.__module__ == leaf.__module__:
+                    snapshot = leaf.__dict__.get("__source__")
+                    path = leaf.__dict__.get("__file__")
+                if snapshot is None and path is None:
+                    path = getfile(cls)
+                add_source(path or cls.__module__, snapshot)
+        for path in (Path(__file__), Path(getfile(LabelTransformer)), Path(getfile(fit_regressor))):
+            add_source(path)
+        digest.update(json.dumps(configuration, sort_keys=True, default=str).encode())
+        for label, payload in sources:
+            digest.update(label.encode())
+            digest.update(payload)
+        columns = [(column, str(dtype)) for column, dtype in prepared.dtypes.items()]
+        digest.update(json.dumps(columns).encode())
+        try:
+            unsafe = [
+                column
+                for column, dtype in prepared.dtypes.items()
+                if pd.api.types.is_object_dtype(dtype) or isinstance(dtype, pd.CategoricalDtype)
+            ]
+            if unsafe:
+                raise TypeError(
+                    f"value hashing loses scalar types or category metadata for columns {unsafe}"
+                )
+            values = pd.util.hash_pandas_object(prepared, index=False).to_numpy()
+        except (TypeError, ValueError) as error:
+            logger.warning(
+                "[%s] Backtest cache reuse disabled: cannot fingerprint inputs (%s: %s); "
+                "using a fresh cache namespace",
+                pair,
+                type(error).__name__,
+                error,
+            )
+            digest.update(uuid4().bytes)
+        else:
+            digest.update(values.data)
+        return digest.hexdigest()
+
+    def start_backtesting(
+        self, dataframe: pd.DataFrame, metadata: dict, dk: FreqaiDataKitchen, strategy: Any
+    ) -> FreqaiDataKitchen:
+        """Scope SDK hooks to one kitchen; keep the upstream training loop intact."""
+        pair = metadata["pair"]
+        populate = dk.use_strategy_to_populate_indicators
+        prepared = populate(strategy, prediction_dataframe=dataframe, pair=pair)
+        fingerprint = self._backtest_fingerprint(prepared, pair, strategy)
+        root = dk.get_full_models_path(self.config)
+        namespace = root / "backtesting" / fingerprint
+        dates = ensure_datetime_series(prepared["date"])
+        if dates.isna().any():
+            raise ValueError(
+                f"[{pair}] Invalid backtest input dates: "
+                f"{dates.isna().sum()} missing or invalid timestamps"
+            )
+        if not dates.is_unique:
+            raise ValueError(
+                f"[{pair}] Invalid backtest input dates: "
+                f"{dates.duplicated().sum()} duplicate timestamps"
+            )
+        if not dates.is_monotonic_increasing:
+            raise ValueError(
+                f"[{pair}] Invalid backtest input dates: timestamps must be in chronological order"
+            )
+        calendar = pd.DatetimeIndex(dates)
+        windows = {
+            training.stopts: prediction
+            for training, prediction in zip(
+                dk.training_timeranges, dk.backtesting_timeranges, strict=True
+            )
+        }
+        check = dk.check_if_backtest_prediction_is_valid
+        append = dk.get_predictions_to_append
+        fill = dk.fill_predictions
+        load = self.dd.load_data
+        hooks = (
+            "get_full_models_path",
+            "use_strategy_to_populate_indicators",
+            "check_if_backtest_prediction_is_valid",
+            "get_predictions_to_append",
+            "fill_predictions",
+        )
+        original = {name: dk.__dict__.get(name) for name in hooks}
+        original_load = self.dd.__dict__.get("load_data")
+
+        def models_path(current: FreqaiDataKitchen, config: dict) -> Path:
+            return namespace
+
+        @wraps(populate)
+        def populated(current: FreqaiDataKitchen, *args, **kwargs) -> pd.DataFrame:
+            requested = kwargs.get("prediction_dataframe")
+            if requested is dataframe or requested is prepared:
+                return prepared
+            if isinstance(requested, pd.DataFrame) and "date" in requested:
+                positions = calendar.get_indexer(
+                    pd.DatetimeIndex(ensure_datetime_series(requested["date"]))
+                )
+                if (positions >= 0).all():
+                    return prepared.iloc[positions]
+            return populate(*args, **kwargs)
+
+        @wraps(check)
+        def valid(current: FreqaiDataKitchen, length: int) -> bool:
+            current.data[self._BACKTEST_CONTEXT_KEY] = fingerprint
+            if not check(length):
+                return False
+            cached = current.get_backtesting_prediction()
+            required = {
+                "date",
+                "do_predict",
+                "holdout_rmse",
+                _PRODUCED_PREDICTION_COLUMN,
+                *LABEL_COLUMNS,
+                *self._DIAGNOSTIC_COLUMNS,
+            }
+            if self.ft_params.get("DI_threshold", 0) > 0:
+                required.add("DI_values")
+            if not required.issubset(cached.columns):
+                return False
+            numeric = required - {"date", _PRODUCED_PREDICTION_COLUMN}
+            if any(not pd.api.types.is_numeric_dtype(cached[column]) for column in numeric):
+                return False
+            status = cached["do_predict"]
+            if (
+                status.isna().any()
+                or not (
+                    np.isfinite(status) & status.le(2) & np.equal(status, np.floor(status))
+                ).all()
+            ):
+                return False
+            if not cached[_PRODUCED_PREDICTION_COLUMN].isin((True, False)).all():
+                return False
+            prediction = windows[self.training_timerange.stopts]
+            expected = dates.loc[(dates >= prediction.startdt) & (dates < prediction.stopdt)]
+            actual = ensure_datetime_series(cached["date"])
+            return bool(
+                len(actual) == len(expected)
+                and actual.notna().all()
+                and actual.is_unique
+                and np.array_equal(actual.to_numpy(), expected.to_numpy())
+            )
+
+        @wraps(append)
+        def appended(
+            current: FreqaiDataKitchen,
+            predictions: pd.DataFrame,
+            status: Any,
+            backtest: pd.DataFrame,
+        ) -> pd.DataFrame:
+            result = append(predictions, status, backtest)
+            produced = current.__dict__.get("_quickadapter_prediction_produced")
+            if produced is None:
+                raise ValueError(
+                    f"[{pair}] Missing _quickadapter_prediction_produced for backtest "
+                    f"predictions: expected {len(result)} rows"
+                )
+            if len(produced) != len(result):
+                raise ValueError(
+                    f"[{pair}] Backtest prediction provenance length mismatch: "
+                    f"expected {len(result)} rows, got {len(produced)}"
+                )
+            result[_PRODUCED_PREDICTION_COLUMN] = produced
+            if "holdout_rmse" not in result:
+                result["holdout_rmse"] = np.nan
+            for column in self._DIAGNOSTIC_COLUMNS:
+                result[column] = backtest[column].to_numpy() if column in backtest else np.nan
+            return result
+
+        @wraps(fill)
+        def filled(current: FreqaiDataKitchen, frame: pd.DataFrame) -> None:
+            recorded = current.full_df
+            fill(prepared)
+            result = current.return_dataframe
+            aligned = recorded.set_index("date").reindex(
+                pd.DatetimeIndex(ensure_datetime_series(result["date"]))
+            )
+            for column in aligned:
+                result[column] = aligned[column].to_numpy()
+            if _PRODUCED_PREDICTION_COLUMN in aligned:
+                result[_PRODUCED_PREDICTION_COLUMN] = (
+                    aligned[_PRODUCED_PREDICTION_COLUMN].fillna(False).to_numpy()
+                )
+            current.return_dataframe = self._normalise_prediction_returns(result)
+
+        @wraps(load)
+        def loaded(drawer: FreqaiDataDrawer, coin: str, kitchen: FreqaiDataKitchen) -> Any:
+            # A backtest window must not inherit another window's cached metadata.
+            if not kitchen.live:
+                drawer.meta_data_dictionary.pop(coin, None)
+                drawer.model_dictionary.pop(coin, None)
+            model = load(coin, kitchen)
+            if model is not None and not kitchen.live:
+                drawer.model_dictionary[coin] = model
+                drawer.meta_data_dictionary[coin] = {
+                    METADATA: kitchen.data,
+                    FEATURE_PIPELINE: kitchen.feature_pipeline,
+                    LABEL_PIPELINE: kitchen.label_pipeline,
+                }
+                drawer.pair_dict[coin].update(
+                    model_filename=kitchen.model_filename,
+                    data_path=str(kitchen.data_path),
+                    trained_timestamp=int(kitchen.model_filename.rsplit("_", 1)[-1]),
+                )
+            return model
+
+        replacements = (models_path, populated, valid, appended, filled)
+        for name, replacement in zip(hooks, replacements, strict=True):
+            dk.__dict__[name] = MethodType(replacement, dk)
+        self.dd.__dict__["load_data"] = MethodType(loaded, self.dd)
+        try:
+            return super().start_backtesting(dataframe, metadata, dk, strategy)
+        finally:
+            for name, previous in original.items():
+                if previous is None:
+                    dk.__dict__.pop(name, None)
+                else:
+                    dk.__dict__[name] = previous
+            if original_load is None:
+                self.dd.__dict__.pop("load_data", None)
+            else:
+                self.dd.__dict__["load_data"] = original_load
+            dk.full_path = root
+
+    def _append_null_backtesting_predictions(
+        self, dk: FreqaiDataKitchen, dataframe_backtest: pd.DataFrame
+    ) -> None:
+        dk.__dict__["_quickadapter_prediction_produced"] = np.zeros(
+            len(dataframe_backtest), dtype=bool
+        )
+        super()._append_null_backtesting_predictions(dk, dataframe_backtest)
+
+    def backtesting_fit_live_predictions(self, dk: FreqaiDataKitchen) -> None:
+        """Replay the last N produced observations, excluding the current candle."""
+        size = self._fit_live_predictions_candles
+        if not size or dk.full_df.empty:
+            return
+        pair = dk.pair
+        frame = dk.full_df
+        frame["date_pred"] = ensure_datetime_series(frame["date"])
+        produced_positions = np.flatnonzero(_produced_prediction_mask(frame))
+        metrics = {
+            column: frame[column].to_numpy(copy=True)
+            for column in ("holdout_rmse",)
+            if column in frame
+        }
+        self._backtest_calibration_starts.pop(pair, None)
+        try:
+            for position in range(len(frame)):
+                end = int(np.searchsorted(produced_positions, position))
+                selected = produced_positions[max(0, end - size) : end]
+                self.dd.historic_predictions[pair] = frame.iloc[selected]
+                self._calibration_current_candles[pair] = frame["date_pred"].iloc[position]
+                self._backtest_current_metrics[pair] = {
+                    column: values[position] for column, values in metrics.items()
+                }
+                self.fit_live_predictions(dk, pair)
+                for label in dk.label_list + dk.unique_class_list:
+                    if pd.api.types.is_string_dtype(frame[label].dtype):
+                        continue
+                    frame.at[frame.index[position], f"{label}_mean"] = dk.data["labels_mean"][label]
+                    frame.at[frame.index[position], f"{label}_std"] = dk.data["labels_std"][label]
+                for column, value in dk.data["extra_returns_per_train"].items():
+                    frame.at[frame.index[position], column] = value
+        finally:
+            self._calibration_current_candles.pop(pair, None)
+            self._backtest_current_metrics.pop(pair, None)
+            self._backtest_calibration_starts.pop(pair, None)
+
+    def predict(
+        self, unfiltered_df: pd.DataFrame, dk: FreqaiDataKitchen, **kwargs
+    ) -> tuple[pd.DataFrame, NDArray[np.int_]]:
+        """Reject incomplete inputs and non-finite outputs in addition to SDK outliers."""
+        self._capture_model_metric(dk, dk.pair)
+        predictions, status = super().predict(unfiltered_df, dk, **kwargs)
+        missing = dk.data["filter_drop_index_prediction"].to_numpy(dtype=bool)
+        finite = np.isfinite(predictions.to_numpy(dtype=float)).all(axis=1)
+        invalid = missing | ~finite
+        expired = status == 2
+        np.subtract(status, invalid & ~expired, out=status)
+        dk.do_predict = status
+        dk.__dict__["_quickadapter_prediction_produced"] = ~invalid & ~expired
+        return predictions, status
+
     def define_data_pipeline(self, threads: int = -1) -> Pipeline:
         scaler = self.ft_params.get("scaler", QuickAdapterRegressorV3.SCALER_DEFAULT)
 
@@ -2275,10 +2818,9 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                 )
                 if known_at_lookahead is not None:
                     train_known_at_lookahead = known_at_lookahead.loc[train_features.index]
-                    train_known_at_position = train_positions.to_numpy(
-                        dtype=np.int64
-                    ) + train_known_at_lookahead.to_numpy(dtype=np.int64)
-                    keep_mask &= train_known_at_position < first_test_position
+                    keep_mask &= train_known_at_lookahead.to_numpy(dtype=np.int64) < (
+                        first_test_position - train_positions.to_numpy(dtype=np.int64)
+                    )
                 else:
                     _log_known_at_none_once(dk.pair, "train_test_split causal guard")
                 (
@@ -2481,6 +3023,12 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
 
         model = self.dd.model_dictionary.get(pair)
         previous = self.dd.pair_dict.get(pair, {})
+        if (
+            not self.live
+            and self._BACKTEST_CONTEXT_KEY in dk.data
+            and not Path(previous.get("data_path", "")).is_relative_to(dk.full_path)
+        ):
+            return None
         if model is None and not previous.get("model_filename"):
             return None
         if not self.live:
@@ -2631,13 +3179,11 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         row_positions = QuickAdapterRegressorV3._row_positions(features, unfiltered_df)
         known_at_lookahead = QuickAdapterRegressorV3._known_at_lookahead(features, unfiltered_df)
         if known_at_lookahead is None:
-            return (
-                row_positions.to_numpy(dtype=np.int64) + self._label_horizon_candles(pair)
-                < cutoff_position
+            return row_positions.to_numpy(dtype=np.int64) < (
+                cutoff_position - self._label_horizon_candles(pair)
             )
-        return (
-            row_positions.to_numpy(dtype=np.int64) + known_at_lookahead.to_numpy(dtype=np.int64)
-            < cutoff_position
+        return known_at_lookahead.to_numpy(dtype=np.int64) < (
+            cutoff_position - row_positions.to_numpy(dtype=np.int64)
         )
 
     def _get_validation_size(self) -> float | int:
@@ -2711,10 +3257,9 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                     dtype=np.int64
                 ) < first_validation_position - self._label_horizon_candles(pair)
             else:
-                train_known_at_position = train_positions.to_numpy(
+                keep_mask = known_at_lookahead.loc[train_features.index].to_numpy(
                     dtype=np.int64
-                ) + known_at_lookahead.loc[train_features.index].to_numpy(dtype=np.int64)
-                keep_mask = train_known_at_position < first_validation_position
+                ) < (first_validation_position - train_positions.to_numpy(dtype=np.int64))
             train_features = train_features.loc[keep_mask]
             train_labels = train_labels.loc[keep_mask]
 
@@ -3184,10 +3729,9 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             )
             if known_at_lookahead is not None:
                 train_known_at_lookahead = known_at_lookahead.iloc[train_idx]
-                train_known_at_position = train_positions.to_numpy(
-                    dtype=np.int64
-                ) + train_known_at_lookahead.to_numpy(dtype=np.int64)
-                keep_mask = train_known_at_position < first_test_position
+                keep_mask = train_known_at_lookahead.to_numpy(dtype=np.int64) < (
+                    first_test_position - train_positions.to_numpy(dtype=np.int64)
+                )
                 (
                     train_features,
                     train_labels,
@@ -3324,6 +3868,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         else:
             self._holdout_rmse[dk.pair] = np.inf
         self._session_fitted_pairs.add(dk.pair)
+        self._metric_model_identity[dk.pair] = self._kitchen_model_identity(dk)
+        self._loaded_holdout_rmse[dk.pair] = self._holdout_rmse[dk.pair]
         dk.data["extra_returns_per_train"]["holdout_rmse"] = self._holdout_rmse[dk.pair]
         if validation_size != 0:
             refit_model_training_parameters = get_refit_model_training_parameters(
@@ -3410,49 +3956,33 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
     def set_initial_historic_predictions(
         self, pred_df: pd.DataFrame, dk: FreqaiDataKitchen, pair: str, strat_df: pd.DataFrame
     ) -> None:
+        self._capture_model_metric(dk, pair)
+        dk.data.setdefault("extra_returns_per_train", {}).setdefault(
+            "holdout_rmse", self._holdout_rmse.get(pair, np.inf)
+        )
         super().set_initial_historic_predictions(pred_df, dk, pair, strat_df)
-        if self.live:
-            self.dd.historic_predictions[pair] = _ensure_produced_prediction_column(
-                self.dd.historic_predictions[pair]
-            )
+        self.dd.historic_predictions[pair][_PRODUCED_PREDICTION_COLUMN] = False
 
     def build_strategy_return_arrays(
         self, dataframe: pd.DataFrame, dk: FreqaiDataKitchen, pair: str, trained_timestamp: int
     ) -> None:
-        """Record real appends and use Freqtrade's current candle for calibration."""
-        had_returns = pair in self.dd.model_return_values
-        if self.live and pair in self.dd.historic_predictions:
-            self.dd.historic_predictions[pair] = _ensure_produced_prediction_column(
-                self.dd.historic_predictions[pair]
-            )
+        """Calibrate before appending, then return explicitly marked prediction gaps."""
+        self._ensure_history_schema(dk, pair)
         if not dataframe.empty:
             self._calibration_current_candles[pair] = pd.to_datetime(
                 dataframe["date"].iloc[-1], utc=True, errors="coerce"
             )
         try:
             super().build_strategy_return_arrays(dataframe, dk, pair, trained_timestamp)
-            if self.live and had_returns:
-                history = self.dd.historic_predictions[pair]
-                current = self._calibration_current_candles.get(pair)
-                # Native append overwrites the last row on the same date, but
-                # returns early if the strategy candle predates the saved one.
-                if (
-                    current is not None
-                    and pd.notna(current)
-                    and not history.empty
-                    and pd.to_datetime(history["date_pred"].iloc[-1], utc=True) == current
-                ):
-                    history.at[history.index[-1], _PRODUCED_PREDICTION_COLUMN] = True
-                    returned = self.dd.model_return_values[pair]
-                    if not returned.empty and _PRODUCED_PREDICTION_COLUMN in returned:
-                        returned.at[returned.index[-1], _PRODUCED_PREDICTION_COLUMN] = True
-            if _PRODUCED_PREDICTION_COLUMN in dk.return_dataframe:
-                dk.return_dataframe = dk.return_dataframe.drop(columns=_PRODUCED_PREDICTION_COLUMN)
+            dk.return_dataframe = self._normalise_prediction_returns(dk.return_dataframe)
         finally:
             self._calibration_current_candles.pop(pair, None)
 
     def _persist_calibration_start(self, pair: str, start: pd.Timestamp) -> None:
         """Persist a UTC boundary without mutating Freqtrade's shared empty extras."""
+        if not self.live:
+            self._backtest_calibration_starts[pair] = start.isoformat()
+            return
         self.dd.get_pair_dict_info(pair)
         pair_info = self.dd.pair_dict[pair]
         extras = pair_info.get("extras", {})
@@ -3512,8 +4042,11 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         decision_time = self._calibration_decision_time(dk, pair, history)
         if decision_time is None:
             return history.iloc[:0]
-        self.dd.get_pair_dict_info(pair)
-        raw_start = self.dd.pair_dict[pair].get("extras", {}).get(self._CALIBRATION_START_KEY)
+        if self.live:
+            self.dd.get_pair_dict_info(pair)
+            raw_start = self.dd.pair_dict[pair].get("extras", {}).get(self._CALIBRATION_START_KEY)
+        else:
+            raw_start = self._backtest_calibration_starts.get(pair)
         start = (
             pd.to_datetime(raw_start, utc=True, errors="coerce")
             if isinstance(raw_start, str)
@@ -3532,7 +4065,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         # Include the persisted boundary itself: history may have been rebuilt
         # after a restart, leaving no rows from the preceding warmup segment.
         gap_starts = eligible.loc[eligible["date_pred"].diff().gt(max_gap), "date_pred"]
-        if not eligible.empty and eligible["date_pred"].iloc[0] - start > max_gap:
+        if self.live and not eligible.empty and eligible["date_pred"].iloc[0] - start > max_gap:
             gap_starts = pd.concat([eligible["date_pred"].iloc[:1], gap_starts])
         if not gap_starts.empty:
             start = gap_starts.iloc[-1]
@@ -3572,20 +4105,23 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                 ),
             )
 
-        history = self.dd.historic_predictions[pair]
-        if self.live:
-            history = _dedupe_historic_predictions_on_date_pred(history, pair)
-            calibration_history = self._calibration_history(
-                dk, pair, history, fit_live_predictions_candles
+        if self.live and pair not in self._session_fitted_pairs:
+            self._capture_model_metric(dk, pair)
+        history = _dedupe_historic_predictions_on_date_pred(
+            self.dd.historic_predictions[pair], pair
+        )
+        calibration_history = self._calibration_history(
+            dk, pair, history, fit_live_predictions_candles
+        )
+        remaining = fit_live_predictions_candles - len(calibration_history)
+        warmed_up = remaining <= 0
+        if not warmed_up:
+            logger.debug(
+                "[%s] Fit live predictions not warmed up: %s produced observations "
+                "until warmup completion",
+                pair,
+                remaining,
             )
-            remaining = fit_live_predictions_candles - len(calibration_history)
-            warmed_up = remaining <= 0
-            if not warmed_up:
-                logger.warning(
-                    f"[{pair}] Fit live predictions not warmed up: {remaining} produced observations until warmup completion"
-                )
-        else:
-            calibration_history = history
         pred_df = calibration_history.tail(fit_live_predictions_candles).reset_index(drop=True)
 
         di_values = pred_df.get("DI_values")
@@ -3704,41 +4240,30 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         ).get("label_natr_multiplier")
 
         current_holdout_rmse = self._holdout_rmse.get(pair)
+        decision_time = self._calibration_decision_time(dk, pair, history)
         if not self.live:
-            holdout_series = dk.full_df.get("holdout_rmse")
-            history_dates = ensure_datetime_series(self.dd.historic_predictions[pair]["date"])
-            full_dates = ensure_datetime_series(dk.full_df["date"])
-            current_dates = full_dates.loc[full_dates > history_dates.max()]
-            if holdout_series is None:
-                logger.warning(
-                    f"[{pair}] 'holdout_rmse' column missing from full_df during "
-                    f"replay; keeping the last in-memory value (may be stale)"
-                )
-            elif not current_dates.empty:
-                current_holdout_rmse = holdout_series.loc[current_dates.index[0]]
-                if pd.isna(current_holdout_rmse):
-                    logger.warning(
-                        f"[{pair}] Replayed holdout_rmse is NaN at "
-                        f"{current_dates.index[0]}; defaulting to inf"
-                    )
+            # The replay cursor supplies original per-candle metadata in constant time.
+            current_metrics = self._backtest_current_metrics.get(pair, {})
+            current_holdout_rmse = current_metrics.get("holdout_rmse", np.inf)
         elif pair not in self._session_fitted_pairs:
-            decision_time = self._calibration_decision_time(dk, pair, history)
-            if decision_time is not None and "holdout_rmse" in history:
-                # Model metadata can predate the current calibration period, but not
-                # come from a future or unproduced prediction.
+            current_holdout_rmse = self._loaded_holdout_rmse.get(pair)
+            if (
+                current_holdout_rmse is None
+                and decision_time is not None
+                and "holdout_rmse" in history
+            ):
                 produced = (
                     _produced_prediction_mask(history)
                     & history["date_pred"].le(decision_time).to_numpy()
                 )
-                holdout_values = pd.to_numeric(
-                    history.loc[produced, "holdout_rmse"], errors="coerce"
-                ).dropna()
+                holdout_values = history.loc[produced, "holdout_rmse"]
                 if not holdout_values.empty:
-                    current_holdout_rmse = float(holdout_values.iloc[-1])
-        holdout_rmse = QuickAdapterRegressorV3.optuna_validate_value(current_holdout_rmse)
-        dk.data["extra_returns_per_train"]["holdout_rmse"] = (
-            holdout_rmse if holdout_rmse is not None else np.inf
+                    current_holdout_rmse = holdout_values.iloc[-1]
+        dk.data["extra_returns_per_train"]["holdout_rmse"] = self._coerce_holdout_rmse(
+            current_holdout_rmse
         )
+        if self.live:
+            self._ensure_history_schema(dk, pair)
 
     def _label_hpo_dataframe_as_of_prediction_time(
         self,
@@ -3747,13 +4272,9 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
     ) -> pd.DataFrame:
         """Return pair OHLCV bounded to the current FreqAI prediction time.
 
-        In backtests, ``historic_predictions`` holds the preceding prediction
-        window and ``dk.full_df`` is the full feature frame; the current
-        prediction time is the first ``dk.full_df`` timestamp strictly after the
-        last recorded prediction. Live and dry-run modes rely on the already
-        point-in-time DataProvider frame. This explicit backtest bound is
-        required because DataProvider otherwise exposes the complete historical
-        timerange.
+        An explicit calibration cursor bounds backtest input, including the current candle.
+        Without a cursor, infer the next candle from the produced-prediction history.
+        Live providers already expose their current candle window.
         """
         pair_dataframe = self.data_provider.get_pair_dataframe(
             pair=pair, timeframe=self.config.get("timeframe")
@@ -3761,6 +4282,13 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         if self.live or pair_dataframe.empty:
             return pair_dataframe
 
+        if pair in self._calibration_current_candles:
+            current_candle = self._calibration_current_candles[pair]
+            if pd.isna(current_candle):
+                return pair_dataframe.iloc[:0]
+            pair_dates = ensure_datetime_series(pair_dataframe["date"])
+            return pair_dataframe.loc[pair_dates <= current_candle]
+
         history_dates = ensure_datetime_series(self.dd.historic_predictions[pair]["date"])
         if history_dates.empty:
             logger.debug(
index 8d65f5b24bb31e5df66cd9bf6397eb6f653f7766..88950f71dce67d4f30828a22bfc767318f78ca42 100644 (file)
@@ -221,7 +221,7 @@ class QuickAdapterV3(IStrategy):
     _ANNOTATION_LINE_OFFSET_CANDLES: Final[int] = 10
 
     def version(self) -> str:
-        return "3.13.0-rc.15"
+        return "3.13.0-rc.16"
 
     timeframe = "5m"
     timeframe_minutes = timeframe_to_minutes(timeframe)
@@ -289,32 +289,28 @@ class QuickAdapterV3(IStrategy):
         return {
             "main_plot": {},
             "subplots": {
-                "accuracy": {
-                    "holdout_rmse": {"color": "violet", "type": "line"},
+                "holdout error": {
+                    "holdout_rmse": {"color": "violet"},
                 },
-                "extrema": {
+                "predicted extrema": {
                     f"{EXTREMA_COLUMN}_maxima_threshold": {
                         "color": "blue",
-                        "type": "line",
                     },
                     f"{EXTREMA_COLUMN}_minima_threshold": {
                         "color": "cyan",
-                        "type": "line",
                     },
-                    EXTREMA_COLUMN: {"color": "orange", "type": "line"},
+                    EXTREMA_COLUMN: {"color": "orange"},
                 },
-                "direction": {
+                "historical target direction": {
                     EXTREMA_DIRECTION_COLUMN: {"color": "steelblue", "type": "bar"},
                     EXTREMA_DIRECTION_SMOOTHED_COLUMN: {
                         "color": "orange",
-                        "type": "line",
                     },
                 },
-                "weight": {
+                "historical label weights": {
                     EXTREMA_WEIGHT_COLUMN: {"color": "steelblue", "type": "bar"},
                     EXTREMA_WEIGHT_SMOOTHED_COLUMN: {
                         "color": "orange",
-                        "type": "line",
                     },
                 },
             },
@@ -2572,13 +2568,17 @@ class QuickAdapterV3(IStrategy):
         leverage = self.config.get("leverage")
         if leverage is None:
             return None
-        if not is_finite_number(leverage):
+        try:
+            converted = float(leverage) if is_finite_number(leverage) else None
+        except (TypeError, ValueError, OverflowError):
+            converted = None
+        if converted is None or not np.isfinite(converted):
             logger.warning(
                 f"Invalid leverage value {leverage!r}: must be a finite number, "
                 "using proposed_leverage"
             )
             return None
-        leverage = float(leverage)
+        leverage = converted
         if leverage < 1.0:
             logger.warning(f"Invalid leverage value {leverage}: must be >= 1.0, clamping to 1.0")
         return leverage
index a8e0833847ee57c805fd7325718a752b47917087..92cc4cc79b86d33977ccf1180fd6a5424da63632 100644 (file)
@@ -5235,7 +5235,7 @@ class _OptunaNamespaces(NamedTuple):
 # underscore on the instance (the class ``_OptunaNamespaces`` stays private).
 _OPTUNA_NAMESPACES: Final[_OptunaNamespaces] = _OptunaNamespaces()
 _OPTUNA_BEST_PARAMS_QUARANTINE_TAG: Final[str] = "corrupt"
-_OPTUNA_BEST_PARAMS_QUARANTINE_TIE_BREAK_LIMIT: Final[int] = 8
+_OPTUNA_BEST_PARAMS_QUARANTINE_TIE_BREAK_LIMIT: Final[int] = 99
 
 
 def _optuna_best_params_path(base_path: Path, pair: str, namespace: OptunaNamespace) -> Path: