]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
feat(quickadapter): configure the exit-stage fraction series (#286)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Mon, 5 Oct 2026 14:00:48 +0000 (16:00 +0200)
committerGitHub <noreply@github.com>
Mon, 5 Oct 2026 14:00:48 +0000 (16:00 +0200)
* feat(quickadapter): configure the exit-stage fraction series

* fix(quickadapter): use integer-power retracement ladder

* fix(quickadapter): correct the exit-stage ladder documentation and guards

* fix(quickadapter): bound the ladder base and index rungs by stage

* refactor(quickadapter): name the ladder after natr_multiplier_fraction

* test(quickadapter): restore partial exit regression guarantees

* test(quickadapter): avoid pinning diagnostic wording

* refactor(quickadapter): align take-profit extension terminology

* test(quickadapter): avoid pinning private diagnostic presentation

* feat(quickadapter): default final retracement fraction to 0.5

* refactor(quickadapter): consistently access static exit stage settings

* docs(quickadapter): keep exit fraction documentation in the configuration table

* chore(quickadapter): bump version to 3.13.0-rc.14

* fix(quickadapter): harmonize exit-pricing diagnostics

* docs(quickadapter): remove incidental startup log detail

* fix(quickadapter): report effective startup configuration

* fix(quickadapter): deduplicate startup horizon warnings

* docs(quickadapter): focus option descriptions on user behavior

CONTRIBUTING.md
quickadapter/.basedpyright/diagnostics.json
quickadapter/README.md
quickadapter/tests/test_regressor_label_config.py
quickadapter/tests/test_strategy_features.py
quickadapter/tests/test_strategy_sizing.py
quickadapter/tests/test_utils_config_resolution.py
quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py
quickadapter/user_data/strategies/QuickAdapterV3.py
quickadapter/user_data/strategies/Utils.py

index 018d0a625e4afeb0034a533c8ecd86f0403b3cd1..bbc05847b516b43d73701d8d812d1abc67dc4493 100644 (file)
@@ -50,6 +50,10 @@ teardown errors; each test still owns isolation of any other mutable state.
 The reward-space analysis suite runs separately with `uv`, without a Freqtrade
 image.
 
+Regression tests should cover behavior, numerical boundaries and error conditions.
+For private diagnostics, check the exception type and relevant input identity;
+do not pin incidental wording, number formatting or arbitrary message lengths.
+
 ## Coverage gate
 
 Both strategy suites enforce their own runtime coverage gates. ReforceXY
index f0977eaa061c7548c870e3a04e2cc1fc6b66476e..2421c5d1a072f28d4c269c98d3f0406a4bc995ee 100644 (file)
   "diagnostics": [
     {
       "endCharacter": 46,
-      "endLine": 1400,
+      "endLine": 1402,
       "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": 1400
+      "startLine": 1402
     },
     {
       "endCharacter": 79,
-      "endLine": 2183,
+      "endLine": 2265,
       "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": 2183
+      "startLine": 2265
     },
     {
       "endCharacter": 72,
-      "endLine": 2185,
+      "endLine": 2267,
       "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": 2185
+      "startLine": 2267
     },
     {
       "endCharacter": 90,
-      "endLine": 2194,
+      "endLine": 2276,
       "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": 2194
+      "startLine": 2276
     },
     {
       "endCharacter": 34,
-      "endLine": 2207,
+      "endLine": 2289,
       "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": 2207
+      "startLine": 2289
     },
     {
       "endCharacter": 32,
-      "endLine": 2208,
+      "endLine": 2290,
       "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": 2208
+      "startLine": 2290
     },
     {
       "endCharacter": 38,
-      "endLine": 2209,
+      "endLine": 2291,
       "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": 2209
+      "startLine": 2291
     },
     {
       "endCharacter": 59,
-      "endLine": 2212,
+      "endLine": 2294,
       "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": 2212
+      "startLine": 2294
     },
     {
       "endCharacter": 34,
-      "endLine": 2246,
+      "endLine": 2328,
       "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": 2246
+      "startLine": 2328
     },
     {
       "endCharacter": 32,
-      "endLine": 2247,
+      "endLine": 2329,
       "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": 2247
+      "startLine": 2329
     },
     {
       "endCharacter": 38,
-      "endLine": 2248,
+      "endLine": 2330,
       "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": 2248
+      "startLine": 2330
     },
     {
       "endCharacter": 39,
-      "endLine": 2249,
+      "endLine": 2331,
       "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": 2249
+      "startLine": 2331
     },
     {
       "endCharacter": 37,
-      "endLine": 2256,
+      "endLine": 2338,
       "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": 2256
+      "startLine": 2338
     },
     {
       "endCharacter": 35,
-      "endLine": 2257,
+      "endLine": 2339,
       "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": 2257
+      "startLine": 2339
     },
     {
       "endCharacter": 41,
-      "endLine": 2258,
+      "endLine": 2340,
       "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": 2258
+      "startLine": 2340
     },
     {
       "endCharacter": 42,
-      "endLine": 2259,
+      "endLine": 2341,
       "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": 2259
+      "startLine": 2341
     },
     {
       "endCharacter": 30,
-      "endLine": 2277,
+      "endLine": 2359,
       "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": 2277
+      "startLine": 2359
     },
     {
       "endCharacter": 31,
-      "endLine": 2278,
+      "endLine": 2360,
       "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": 2278
+      "startLine": 2360
     },
     {
       "endCharacter": 33,
-      "endLine": 2284,
+      "endLine": 2366,
       "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": 2284
+      "startLine": 2366
     },
     {
       "endCharacter": 34,
-      "endLine": 2285,
+      "endLine": 2367,
       "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": 2285
+      "startLine": 2367
     },
     {
       "endCharacter": 26,
-      "endLine": 2292,
+      "endLine": 2374,
       "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": 2292
+      "startLine": 2374
     },
     {
       "endCharacter": 25,
-      "endLine": 2293,
+      "endLine": 2375,
       "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": 2293
+      "startLine": 2375
     },
     {
       "endCharacter": 24,
-      "endLine": 2294,
+      "endLine": 2376,
       "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": 2294
+      "startLine": 2376
     },
     {
       "endCharacter": 23,
-      "endLine": 2295,
+      "endLine": 2377,
       "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": 2295
+      "startLine": 2377
     },
     {
       "endCharacter": 87,
-      "endLine": 2621,
+      "endLine": 2703,
       "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": 2621
+      "startLine": 2703
     },
     {
       "endCharacter": 68,
-      "endLine": 2622,
+      "endLine": 2704,
       "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": 2622
+      "startLine": 2704
     },
     {
       "endCharacter": 63,
-      "endLine": 2633,
+      "endLine": 2715,
       "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": 2633
+      "startLine": 2715
     },
     {
       "endCharacter": 47,
-      "endLine": 2635,
+      "endLine": 2717,
       "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": 2635
+      "startLine": 2717
     },
     {
       "endCharacter": 47,
-      "endLine": 2635,
+      "endLine": 2717,
       "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": 2635
+      "startLine": 2717
     },
     {
       "endCharacter": 43,
-      "endLine": 2636,
+      "endLine": 2718,
       "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": 2636
+      "startLine": 2718
     },
     {
       "endCharacter": 43,
-      "endLine": 2636,
+      "endLine": 2718,
       "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": 2636
+      "startLine": 2718
     },
     {
       "endCharacter": 31,
-      "endLine": 2657,
+      "endLine": 2739,
       "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": 2657
+      "startLine": 2739
     },
     {
       "endCharacter": 31,
-      "endLine": 2657,
+      "endLine": 2739,
       "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": 2657
+      "startLine": 2739
     },
     {
       "endCharacter": 60,
-      "endLine": 2657,
+      "endLine": 2739,
       "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": 2657
+      "startLine": 2739
     },
     {
       "endCharacter": 60,
-      "endLine": 2657,
+      "endLine": 2739,
       "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": 2657
+      "startLine": 2739
     },
     {
       "endCharacter": 73,
-      "endLine": 2669,
+      "endLine": 2751,
       "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": 2669
+      "startLine": 2751
     },
     {
       "endCharacter": 73,
-      "endLine": 2669,
+      "endLine": 2751,
       "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": 2669
+      "startLine": 2751
     },
     {
       "endCharacter": 83,
-      "endLine": 2670,
+      "endLine": 2752,
       "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": 2670
+      "startLine": 2752
     },
     {
       "endCharacter": 83,
-      "endLine": 2670,
+      "endLine": 2752,
       "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": 2670
+      "startLine": 2752
     },
     {
       "endCharacter": 46,
-      "endLine": 2905,
+      "endLine": 2987,
       "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": 2905
+      "startLine": 2987
     },
     {
       "endCharacter": 46,
-      "endLine": 2905,
+      "endLine": 2987,
       "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": 2905
+      "startLine": 2987
     },
     {
       "endCharacter": 46,
-      "endLine": 2905,
+      "endLine": 2987,
       "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": 2905
+      "startLine": 2987
     },
     {
       "endCharacter": 46,
-      "endLine": 2905,
+      "endLine": 2987,
       "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": 2905
+      "startLine": 2987
     },
     {
       "endCharacter": 46,
-      "endLine": 2905,
+      "endLine": 2987,
       "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": 2905
+      "startLine": 2987
     },
     {
       "endCharacter": 46,
-      "endLine": 2905,
+      "endLine": 2987,
       "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": 2905
+      "startLine": 2987
     },
     {
       "endCharacter": 46,
-      "endLine": 2905,
+      "endLine": 2987,
       "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": 2905
+      "startLine": 2987
     },
     {
       "endCharacter": 41,
-      "endLine": 2912,
+      "endLine": 2994,
       "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": 2912
+      "startLine": 2994
     },
     {
       "endCharacter": 39,
-      "endLine": 2961,
+      "endLine": 3043,
       "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": 2961
+      "startLine": 3043
     },
     {
       "endCharacter": 19,
-      "endLine": 3071,
+      "endLine": 3153,
       "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": 3071
+      "startLine": 3153
     },
     {
       "endCharacter": 21,
-      "endLine": 3183,
+      "endLine": 3265,
       "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": 3183
+      "startLine": 3265
     },
     {
       "endCharacter": 21,
-      "endLine": 3184,
+      "endLine": 3266,
       "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": 3184
+      "startLine": 3266
     },
     {
       "endCharacter": 33,
-      "endLine": 3185,
+      "endLine": 3267,
       "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": 3185
+      "startLine": 3267
     },
     {
       "endCharacter": 32,
-      "endLine": 3186,
+      "endLine": 3268,
       "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": 3186
+      "startLine": 3268
     },
     {
       "endCharacter": 32,
-      "endLine": 3187,
+      "endLine": 3269,
       "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": 3187
+      "startLine": 3269
     },
     {
       "endCharacter": 38,
-      "endLine": 3188,
+      "endLine": 3270,
       "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": 3188
+      "startLine": 3270
     },
     {
       "endCharacter": 24,
-      "endLine": 3209,
+      "endLine": 3291,
       "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": 3209
+      "startLine": 3291
     },
     {
       "endCharacter": 24,
-      "endLine": 3210,
+      "endLine": 3292,
       "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": 3210
+      "startLine": 3292
     },
     {
       "endCharacter": 30,
-      "endLine": 3211,
+      "endLine": 3293,
       "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": 3211
+      "startLine": 3293
     },
     {
       "endCharacter": 15,
-      "endLine": 3217,
+      "endLine": 3299,
       "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": 3217
+      "startLine": 3299
     },
     {
       "endCharacter": 15,
-      "endLine": 3218,
+      "endLine": 3300,
       "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": 3218
+      "startLine": 3300
     },
     {
       "endCharacter": 39,
-      "endLine": 3219,
+      "endLine": 3301,
       "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": 3219
+      "startLine": 3301
     },
     {
       "endCharacter": 38,
-      "endLine": 3229,
+      "endLine": 3311,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "\"columns\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 31,
-      "startLine": 3229
+      "startLine": 3311
     },
     {
       "endCharacter": 34,
-      "endLine": 3230,
+      "endLine": 3312,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "\"index\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 29,
-      "startLine": 3230
+      "startLine": 3312
     },
     {
       "endCharacter": 82,
-      "endLine": 3232,
+      "endLine": 3314,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "\"copy\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 78,
-      "startLine": 3232
+      "startLine": 3314
     },
     {
       "endCharacter": 62,
-      "endLine": 3675,
+      "endLine": 3757,
       "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": 3675
+      "startLine": 3757
     },
     {
       "endCharacter": 68,
-      "endLine": 4055,
+      "endLine": 4137,
       "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": 4051
+      "startLine": 4133
     },
     {
       "endCharacter": 13,
-      "endLine": 4084,
+      "endLine": 4166,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "No overloads for \"cdist\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 19,
-      "startLine": 4079
+      "startLine": 4161
     },
     {
       "endCharacter": 38,
-      "endLine": 4082,
+      "endLine": 4164,
       "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": 4082
+      "startLine": 4164
     },
     {
       "endCharacter": 9,
-      "endLine": 4187,
+      "endLine": 4269,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "No overloads for \"pdist\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 36,
-      "startLine": 4185
+      "startLine": 4267
     },
     {
       "endCharacter": 42,
-      "endLine": 4186,
+      "endLine": 4268,
       "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": 4186
+      "startLine": 4268
     },
     {
       "endCharacter": 9,
-      "endLine": 4264,
+      "endLine": 4346,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "No overloads for \"cdist\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 15,
-      "startLine": 4259
+      "startLine": 4341
     },
     {
       "endCharacter": 34,
-      "endLine": 4262,
+      "endLine": 4344,
       "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": 4262
+      "startLine": 4344
     },
     {
       "endCharacter": 99,
-      "endLine": 4453,
+      "endLine": 4535,
       "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py",
       "message": "No overloads for \"pdist\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 4453
+      "startLine": 4535
     },
     {
       "endCharacter": 79,
-      "endLine": 4453,
+      "endLine": 4535,
       "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": 4453
+      "startLine": 4535
     },
     {
       "endCharacter": 55,
-      "endLine": 223,
+      "endLine": 225,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Type \"int\" is not assignable to declared type \"cached_property[int]\"\n  \"int\" is not assignable to \"cached_property[int]\"",
       "rule": "reportAssignmentType",
       "severity": "error",
       "startCharacter": 24,
-      "startLine": 223
+      "startLine": 225
     },
     {
       "endCharacter": 15,
-      "endLine": 259,
+      "endLine": 269,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Class variable \"minimal_roi\" overrides instance variable of same name in class \"IStrategy\"",
       "rule": "reportIncompatibleVariableOverride",
       "severity": "error",
       "startCharacter": 4,
-      "startLine": 259
+      "startLine": 269
     },
     {
       "endCharacter": 72,
-      "endLine": 259,
+      "endLine": 269,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"*\" not supported for types \"cached_property[int]\" and \"Literal[864]\"",
       "rule": "reportOperatorIssue",
       "severity": "error",
       "startCharacter": 49,
-      "startLine": 259
+      "startLine": 269
     },
     {
       "endCharacter": 64,
-      "endLine": 272,
+      "endLine": 282,
       "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_minutes\"\n  Type \"Any | None\" is not assignable to type \"str\"\n    \"None\" is not assignable to \"str\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 36,
-      "startLine": 272
+      "startLine": 282
     },
     {
       "endCharacter": 17,
-      "endLine": 281,
+      "endLine": 291,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "\"can_short\" overrides symbol of same name in class \"IStrategy\"\n  \"property\" is not assignable to \"bool\"",
       "rule": "reportIncompatibleVariableOverride",
       "severity": "error",
       "startCharacter": 8,
-      "startLine": 281
+      "startLine": 291
     },
     {
       "endCharacter": 19,
-      "endLine": 285,
+      "endLine": 295,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "\"plot_config\" overrides symbol of same name in class \"IStrategy\"\n  \"cached_property[dict[str, Any]]\" is not assignable to \"dict[Unknown, Unknown]\"",
       "rule": "reportIncompatibleVariableOverride",
       "severity": "error",
       "startCharacter": 8,
-      "startLine": 285
+      "startLine": 295
     },
     {
       "endCharacter": 19,
-      "endLine": 329,
+      "endLine": 339,
       "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": 329
+      "startLine": 339
     },
     {
       "endCharacter": 28,
-      "endLine": 402,
+      "endLine": 412,
       "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": 402
+      "startLine": 412
     },
     {
       "endCharacter": 44,
-      "endLine": 461,
+      "endLine": 491,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"/\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 461
+      "startLine": 491
     },
     {
       "endCharacter": 38,
-      "endLine": 716,
+      "endLine": 766,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "\"to_numpy\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 30,
-      "startLine": 716
+      "startLine": 766
     },
     {
       "endCharacter": 44,
-      "endLine": 749,
+      "endLine": 799,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"-\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 38,
-      "startLine": 749
+      "startLine": 799
     },
     {
       "endCharacter": 38,
-      "endLine": 757,
+      "endLine": 807,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Object of type \"None\" is not subscriptable",
       "rule": "reportOptionalSubscript",
       "severity": "error",
       "startCharacter": 36,
-      "startLine": 757
+      "startLine": 807
     },
     {
       "endCharacter": 39,
-      "endLine": 758,
+      "endLine": 808,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Object of type \"None\" is not subscriptable",
       "rule": "reportOptionalSubscript",
       "severity": "error",
       "startCharacter": 37,
-      "startLine": 758
+      "startLine": 808
     },
     {
       "endCharacter": 38,
-      "endLine": 759,
+      "endLine": 809,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Object of type \"None\" is not subscriptable",
       "rule": "reportOptionalSubscript",
       "severity": "error",
       "startCharacter": 36,
-      "startLine": 759
+      "startLine": 809
     },
     {
       "endCharacter": 44,
-      "endLine": 782,
+      "endLine": 832,
       "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": 782
+      "startLine": 832
     },
     {
       "endCharacter": 66,
-      "endLine": 782,
+      "endLine": 832,
       "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": 782
+      "startLine": 832
     },
     {
       "endCharacter": 72,
-      "endLine": 782,
+      "endLine": 832,
       "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": 782
+      "startLine": 832
     },
     {
       "endCharacter": 56,
-      "endLine": 788,
+      "endLine": 838,
       "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": 788
+      "startLine": 838
     },
     {
       "endCharacter": 58,
-      "endLine": 789,
+      "endLine": 839,
       "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": 789
+      "startLine": 839
     },
     {
       "endCharacter": 57,
-      "endLine": 790,
+      "endLine": 840,
       "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": 790
+      "startLine": 840
     },
     {
       "endCharacter": 44,
-      "endLine": 793,
+      "endLine": 843,
       "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": 793
+      "startLine": 843
     },
     {
       "endCharacter": 44,
-      "endLine": 794,
+      "endLine": 844,
       "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": 794
+      "startLine": 844
     },
     {
       "endCharacter": 44,
-      "endLine": 795,
+      "endLine": 845,
       "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": 795
+      "startLine": 845
     },
     {
       "endCharacter": 61,
-      "endLine": 796,
+      "endLine": 846,
       "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": 796
+      "startLine": 846
     },
     {
       "endCharacter": 61,
-      "endLine": 797,
+      "endLine": 847,
       "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": 797
+      "startLine": 847
     },
     {
       "endCharacter": 61,
-      "endLine": 798,
+      "endLine": 848,
       "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": 798
+      "startLine": 848
     },
     {
       "endCharacter": 81,
-      "endLine": 806,
+      "endLine": 856,
       "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": 806
+      "startLine": 856
     },
     {
       "endCharacter": 67,
-      "endLine": 819,
+      "endLine": 869,
       "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": 819
+      "startLine": 869
     },
     {
       "endCharacter": 68,
-      "endLine": 820,
+      "endLine": 870,
       "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": 820
+      "startLine": 870
     },
     {
       "endCharacter": 67,
-      "endLine": 821,
+      "endLine": 871,
       "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": 821
+      "startLine": 871
     },
     {
       "endCharacter": 44,
-      "endLine": 822,
+      "endLine": 872,
       "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": 822
+      "startLine": 872
     },
     {
       "endCharacter": 56,
-      "endLine": 823,
+      "endLine": 873,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "No overloads for \"__call__\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 31,
-      "startLine": 823
+      "startLine": 873
     },
     {
       "endCharacter": 47,
-      "endLine": 823,
+      "endLine": 873,
       "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": 823
+      "startLine": 873
     },
     {
       "endCharacter": 55,
-      "endLine": 823,
+      "endLine": 873,
       "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": 823
+      "startLine": 873
     },
     {
       "endCharacter": 64,
-      "endLine": 824,
+      "endLine": 874,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "No overloads for \"__call__\" match the provided arguments",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 39,
-      "startLine": 824
+      "startLine": 874
     },
     {
       "endCharacter": 55,
-      "endLine": 824,
+      "endLine": 874,
       "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": 824
+      "startLine": 874
     },
     {
       "endCharacter": 63,
-      "endLine": 824,
+      "endLine": 874,
       "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": 824
+      "startLine": 874
     },
     {
       "endCharacter": 55,
-      "endLine": 832,
+      "endLine": 882,
       "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": 832
+      "startLine": 882
     },
     {
       "endCharacter": 55,
-      "endLine": 833,
+      "endLine": 883,
       "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": 833
+      "startLine": 883
     },
     {
       "endCharacter": 55,
-      "endLine": 834,
+      "endLine": 884,
       "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": 834
+      "startLine": 884
     },
     {
       "endCharacter": 55,
-      "endLine": 835,
+      "endLine": 885,
       "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": 835
+      "startLine": 885
     },
     {
       "endCharacter": 55,
-      "endLine": 836,
+      "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": 49,
-      "startLine": 836
+      "startLine": 886
     },
     {
       "endCharacter": 55,
-      "endLine": 837,
+      "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": 49,
-      "startLine": 837
+      "startLine": 887
     },
     {
       "endCharacter": 50,
-      "endLine": 866,
+      "endLine": 916,
       "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": 866
+      "startLine": 916
     },
     {
       "endCharacter": 50,
-      "endLine": 866,
+      "endLine": 916,
       "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": 866
+      "startLine": 916
     },
     {
       "endCharacter": 29,
-      "endLine": 867,
+      "endLine": 917,
       "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": 867
+      "startLine": 917
     },
     {
       "endCharacter": 29,
-      "endLine": 867,
+      "endLine": 917,
       "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": 867
+      "startLine": 917
     },
     {
       "endCharacter": 60,
-      "endLine": 904,
+      "endLine": 954,
       "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": 904
+      "startLine": 954
     },
     {
       "endCharacter": 60,
-      "endLine": 904,
+      "endLine": 954,
       "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": 904
+      "startLine": 954
     },
     {
       "endCharacter": 35,
-      "endLine": 905,
+      "endLine": 955,
       "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": 905
+      "startLine": 955
     },
     {
       "endCharacter": 35,
-      "endLine": 905,
+      "endLine": 955,
       "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": 905
+      "startLine": 955
     },
     {
       "endCharacter": 41,
-      "endLine": 1130,
+      "endLine": 1180,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"<\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 1130
+      "startLine": 1180
     },
     {
       "endCharacter": 41,
-      "endLine": 1140,
+      "endLine": 1190,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \">\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 1140
+      "startLine": 1190
     },
     {
       "endCharacter": 66,
-      "endLine": 1153,
+      "endLine": 1203,
       "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": 1153
+      "startLine": 1203
     },
     {
       "endCharacter": 70,
-      "endLine": 1284,
+      "endLine": 1334,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Expected 1 more positional argument",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 48,
-      "startLine": 1284
+      "startLine": 1334
     },
     {
       "endCharacter": 74,
-      "endLine": 1348,
+      "endLine": 1398,
       "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": 1348
+      "startLine": 1398
     },
     {
       "endCharacter": 74,
-      "endLine": 1375,
+      "endLine": 1425,
       "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": 1375
+      "startLine": 1425
     },
     {
       "endCharacter": 96,
-      "endLine": 1427,
+      "endLine": 1477,
       "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": 1427
+      "startLine": 1477
     },
     {
       "endCharacter": 77,
-      "endLine": 1461,
+      "endLine": 1507,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"<=\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 57,
-      "startLine": 1461
+      "startLine": 1507
     },
     {
       "endCharacter": 9,
-      "endLine": 1466,
+      "endLine": 1512,
       "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": 1464
+      "startLine": 1510
     },
     {
       "endCharacter": 33,
-      "endLine": 1465,
+      "endLine": 1511,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"-\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 13,
-      "startLine": 1465
+      "startLine": 1511
     },
     {
       "endCharacter": 67,
-      "endLine": 1471,
+      "endLine": 1517,
       "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": 1471
+      "startLine": 1517
     },
     {
       "endCharacter": 67,
-      "endLine": 1471,
+      "endLine": 1517,
       "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": 1471
+      "startLine": 1517
     },
     {
       "endCharacter": 64,
-      "endLine": 1625,
+      "endLine": 1671,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "\"isoformat\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 55,
-      "startLine": 1625
+      "startLine": 1671
     },
     {
       "endCharacter": 37,
-      "endLine": 1676,
+      "endLine": 1722,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"<=\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 15,
-      "startLine": 1676
+      "startLine": 1722
     },
     {
       "endCharacter": 48,
-      "endLine": 1678,
+      "endLine": 1724,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"<=\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 15,
-      "startLine": 1678
+      "startLine": 1724
     },
     {
       "endCharacter": 96,
-      "endLine": 1858,
+      "endLine": 1904,
       "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": 1858
+      "startLine": 1904
     },
     {
       "endCharacter": 39,
-      "endLine": 1935,
+      "endLine": 1981,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"+\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 21,
-      "startLine": 1935
+      "startLine": 1981
     },
     {
       "endCharacter": 90,
-      "endLine": 1935,
+      "endLine": 1981,
       "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": 1935
+      "startLine": 1981
     },
     {
       "endCharacter": 96,
-      "endLine": 2239,
+      "endLine": 2285,
       "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": 2239
+      "startLine": 2285
     },
     {
       "endCharacter": 85,
-      "endLine": 2264,
+      "endLine": 2310,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "\"isoformat\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 76,
-      "startLine": 2264
+      "startLine": 2310
     },
     {
       "endCharacter": 47,
-      "endLine": 2270,
+      "endLine": 2316,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \"<\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 2270
+      "startLine": 2316
     },
     {
       "endCharacter": 42,
-      "endLine": 2274,
+      "endLine": 2320,
       "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": 2274
+      "startLine": 2320
     },
     {
       "endCharacter": 42,
-      "endLine": 2275,
+      "endLine": 2321,
       "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": 2275
+      "startLine": 2321
     },
     {
       "endCharacter": 69,
-      "endLine": 2277,
+      "endLine": 2323,
       "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": 2277
+      "startLine": 2323
     },
     {
       "endCharacter": 47,
-      "endLine": 2288,
+      "endLine": 2334,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Operator \">\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 2288
+      "startLine": 2334
     },
     {
       "endCharacter": 43,
-      "endLine": 2292,
+      "endLine": 2338,
       "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": 2292
+      "startLine": 2338
     },
     {
       "endCharacter": 42,
-      "endLine": 2293,
+      "endLine": 2339,
       "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": 2293
+      "startLine": 2339
     },
     {
       "endCharacter": 69,
-      "endLine": 2295,
+      "endLine": 2341,
       "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": 2295
+      "startLine": 2341
     },
     {
       "endCharacter": 52,
-      "endLine": 2324,
+      "endLine": 2370,
       "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": 2324
+      "startLine": 2370
     },
     {
       "endCharacter": 96,
-      "endLine": 2444,
+      "endLine": 2490,
       "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": 2444
+      "startLine": 2490
     },
     {
       "endCharacter": 43,
-      "endLine": 2453,
+      "endLine": 2499,
       "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": 2453
+      "startLine": 2499
     },
     {
       "endCharacter": 69,
-      "endLine": 2455,
+      "endLine": 2501,
       "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": 2455
+      "startLine": 2501
     },
     {
       "endCharacter": 32,
-      "endLine": 2531,
+      "endLine": 2577,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"LocalTrade\" cannot be assigned to parameter \"trade\" of type \"Trade\" in function \"get_trade_annotation_line_start_date\"\n  \"LocalTrade\" is not assignable to \"Trade\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 27,
-      "startLine": 2531
+      "startLine": 2577
     },
     {
       "endCharacter": 72,
-      "endLine": 2534,
+      "endLine": 2580,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"LocalTrade\" cannot be assigned to parameter \"trade\" of type \"Trade\" in function \"get_trade_exit_stage\"\n  \"LocalTrade\" is not assignable to \"Trade\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 67,
-      "startLine": 2534
+      "startLine": 2580
     },
     {
       "endCharacter": 36,
-      "endLine": 2541,
+      "endLine": 2587,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"LocalTrade\" cannot be assigned to parameter \"trade\" of type \"Trade\" in function \"get_take_profit_target\"\n  \"LocalTrade\" is not assignable to \"Trade\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 31,
-      "startLine": 2541
+      "startLine": 2587
     },
     {
       "endCharacter": 71,
-      "endLine": 2572,
+      "endLine": 2618,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"LocalTrade\" cannot be assigned to parameter \"trade\" of type \"Trade\" in function \"get_trade_entry_date\"\n  \"LocalTrade\" is not assignable to \"Trade\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 66,
-      "startLine": 2572
+      "startLine": 2618
     },
     {
       "endCharacter": 32,
-      "endLine": 2603,
+      "endLine": 2649,
       "file": "quickadapter/user_data/strategies/QuickAdapterV3.py",
       "message": "Argument of type \"LocalTrade\" cannot be assigned to parameter \"trade\" of type \"Trade\" in function \"get_take_profit_target\"\n  \"LocalTrade\" is not assignable to \"Trade\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 27,
-      "startLine": 2603
+      "startLine": 2649
     },
     {
       "endCharacter": 35,
     },
     {
       "endCharacter": 98,
-      "endLine": 1638,
+      "endLine": 1710,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Type \"bool | bool_\" is not assignable to return type \"bool\"\n  Type \"bool | bool_\" is not assignable to type \"bool\"\n    \"numpy.bool[builtins.bool]\" is not assignable to \"bool\"",
       "rule": "reportReturnType",
       "severity": "error",
       "startCharacter": 15,
-      "startLine": 1638
+      "startLine": 1710
     },
     {
       "endCharacter": 33,
-      "endLine": 1668,
+      "endLine": 1744,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Argument of type \"Scalar\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Scalar\" is not assignable to type \"ConvertibleToInt\"\n    Type \"complex\" is not assignable to type \"ConvertibleToInt\"\n      \"complex\" is not assignable to \"str\"\n      \"complex\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"complex\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"complex\" is incompatible with protocol \"SupportsIndex\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 20,
-      "startLine": 1668
+      "startLine": 1744
     },
     {
       "endCharacter": 33,
-      "endLine": 1668,
+      "endLine": 1744,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Argument of type \"Scalar\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Scalar\" is not assignable to type \"ConvertibleToInt\"\n    Type \"complex\" is not assignable to type \"ConvertibleToInt\"\n      \"complex\" is not assignable to \"str\"\n      \"complex\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"complex\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"complex\" is incompatible with protocol \"SupportsIndex\"\n  ...",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 20,
-      "startLine": 1668
+      "startLine": 1744
     },
     {
       "endCharacter": 23,
-      "endLine": 1680,
+      "endLine": 1756,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Type \"float\" is not assignable to return type \"T@get_distance\"\n  Type \"float\" is not assignable to type \"T@get_distance\"",
       "rule": "reportReturnType",
       "severity": "error",
       "startCharacter": 11,
-      "startLine": 1680
+      "startLine": 1756
     },
     {
       "endCharacter": 83,
-      "endLine": 2533,
+      "endLine": 2609,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Argument of type \"str\" cannot be assigned to parameter \"key\" of type \"CombinedMetric\" in function \"__getitem__\"\n  Type \"str\" is not assignable to type \"CombinedMetric\"\n    \"str\" is not assignable to type \"Literal['amplitude']\"\n    \"str\" is not assignable to type \"Literal['amplitude_threshold_ratio']\"\n    \"str\" is not assignable to type \"Literal['volume_rate']\"\n    \"str\" is not assignable to type \"Literal['speed']\"\n    \"str\" is not assignable to type \"Literal['efficiency_ratio']\"\n    \"str\" is not assignable to type \"Literal['volume_weighted_efficiency_ratio']\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 58,
-      "startLine": 2533
+      "startLine": 2609
     },
     {
       "endCharacter": 38,
-      "endLine": 3423,
+      "endLine": 3499,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Cannot access attribute \"__func__\" for class \"FunctionType\"\n  Attribute \"__func__\" is unknown",
       "rule": "reportFunctionMemberAccess",
       "severity": "error",
       "startCharacter": 30,
-      "startLine": 3423
+      "startLine": 3499
     },
     {
       "endCharacter": 34,
-      "endLine": 3425,
+      "endLine": 3501,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Cannot access attribute \"__func__\" for class \"FunctionType\"\n  Attribute \"__func__\" is unknown",
       "rule": "reportFunctionMemberAccess",
       "severity": "error",
       "startCharacter": 26,
-      "startLine": 3425
+      "startLine": 3501
     },
     {
       "endCharacter": 55,
-      "endLine": 3655,
+      "endLine": 3731,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"rolling\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 48,
-      "startLine": 3655
+      "startLine": 3731
     },
     {
       "endCharacter": 38,
-      "endLine": 3680,
+      "endLine": 3756,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"rolling\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 31,
-      "startLine": 3680
+      "startLine": 3756
     },
     {
       "endCharacter": 55,
-      "endLine": 3706,
+      "endLine": 3782,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"rolling\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 48,
-      "startLine": 3706
+      "startLine": 3782
     },
     {
       "endCharacter": 56,
-      "endLine": 3707,
+      "endLine": 3783,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"rolling\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 49,
-      "startLine": 3707
+      "startLine": 3783
     },
     {
       "endCharacter": 33,
-      "endLine": 3779,
+      "endLine": 3855,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Argument of type \"Series[Any] | NDArray[floating[Any]]\" cannot be assigned to parameter \"series\" of type \"Series[Any]\" in function \"calculate_zero_lag\"\n  Type \"Series[Any] | NDArray[floating[Any]]\" is not assignable to type \"Series[Any]\"\n    \"ndarray[_AnyShape, dtype[floating[Any]]]\" is not assignable to \"Series[Any]\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 27,
-      "startLine": 3779
+      "startLine": 3855
     },
     {
       "endCharacter": 69,
-      "endLine": 3779,
+      "endLine": 3855,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Expected 1 more positional argument",
       "rule": "reportCallIssue",
       "severity": "error",
       "startCharacter": 59,
-      "startLine": 3779
+      "startLine": 3855
     },
     {
       "endCharacter": 40,
-      "endLine": 3846,
+      "endLine": 3922,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"series\" of type \"Series[Any]\" in function \"calculate_zero_lag\"\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": 35,
-      "startLine": 3846
+      "startLine": 3922
     },
     {
       "endCharacter": 38,
-      "endLine": 3847,
+      "endLine": 3923,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"series\" of type \"Series[Any]\" in function \"calculate_zero_lag\"\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": 34,
-      "startLine": 3847
+      "startLine": 3923
     },
     {
       "endCharacter": 42,
-      "endLine": 3848,
+      "endLine": 3924,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"series\" of type \"Series[Any]\" in function \"calculate_zero_lag\"\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": 36,
-      "startLine": 3848
+      "startLine": 3924
     },
     {
       "endCharacter": 45,
-      "endLine": 3850,
+      "endLine": 3926,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"index\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 40,
-      "startLine": 3850
+      "startLine": 3926
     },
     {
       "endCharacter": 33,
-      "endLine": 3852,
+      "endLine": 3928,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"iloc\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 29,
-      "startLine": 3852
+      "startLine": 3928
     },
     {
       "endCharacter": 31,
-      "endLine": 3853,
+      "endLine": 3929,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"iloc\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 27,
-      "startLine": 3853
+      "startLine": 3929
     },
     {
       "endCharacter": 48,
-      "endLine": 3858,
+      "endLine": 3934,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"index\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 43,
-      "startLine": 3858
+      "startLine": 3934
     },
     {
       "endCharacter": 40,
-      "endLine": 3859,
+      "endLine": 3935,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"iloc\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 36,
-      "startLine": 3859
+      "startLine": 3935
     },
     {
       "endCharacter": 59,
-      "endLine": 3861,
+      "endLine": 3937,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Cannot access attribute \"iloc\" for class \"NDArray[Any]\"\n  Attribute \"iloc\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 55,
-      "startLine": 3861
+      "startLine": 3937
     },
     {
       "endCharacter": 34,
-      "endLine": 3863,
+      "endLine": 3939,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Cannot access attribute \"iloc\" for class \"NDArray[Any]\"\n  Attribute \"iloc\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 30,
-      "startLine": 3863
+      "startLine": 3939
     },
     {
       "endCharacter": 51,
-      "endLine": 3863,
+      "endLine": 3939,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"iloc\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 47,
-      "startLine": 3863
+      "startLine": 3939
     },
     {
       "endCharacter": 72,
-      "endLine": 3863,
+      "endLine": 3939,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Cannot access attribute \"iloc\" for class \"NDArray[Any]\"\n  Attribute \"iloc\" is unknown",
       "rule": "reportAttributeAccessIssue",
       "severity": "error",
       "startCharacter": 68,
-      "startLine": 3863
+      "startLine": 3939
     },
     {
       "endCharacter": 40,
-      "endLine": 3930,
+      "endLine": 4006,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Type \"(Series[Any] | NDArray[floating[Any]], int) -> (Series[Any] | NDArray[floating[Any]])\" is not assignable to declared type \"(series: Unknown, timeperiod: Unknown) -> Series[Any]\"\n  Type \"(Series[Any] | NDArray[floating[Any]], int) -> (Series[Any] | NDArray[floating[Any]])\" is not assignable to type \"(series: Unknown, timeperiod: Unknown) -> Series[Any]\"\n    Missing keyword parameter \"series\"\n    Missing keyword parameter \"timeperiod\"\n      Position-only parameter mismatch; parameter \"series\" is not position-only\n      Position-only parameter mismatch; parameter \"timeperiod\" is not position-only\n      Position-only parameter mismatch; expected 2 but received 0\n      Function return type \"Series[Any] | NDArray[floating[Any]]\" is incompatible with type \"Series[Any]\"\n        Type \"Series[Any] | NDArray[floating[Any]]\" is not assignable to type \"Series[Any]\"\n  ...",
       "rule": "reportAssignmentType",
       "severity": "error",
       "startCharacter": 20,
-      "startLine": 3930
+      "startLine": 4006
     },
     {
       "endCharacter": 33,
-      "endLine": 3932,
+      "endLine": 4008,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Type \"(Series[Any] | NDArray[floating[Any]], int) -> (Series[Any] | NDArray[floating[Any]])\" is not assignable to declared type \"(series: Unknown, timeperiod: Unknown) -> Series[Any]\"\n  Type \"(Series[Any] | NDArray[floating[Any]], int) -> (Series[Any] | NDArray[floating[Any]])\" is not assignable to type \"(series: Unknown, timeperiod: Unknown) -> Series[Any]\"\n    Missing keyword parameter \"series\"\n    Missing keyword parameter \"timeperiod\"\n      Position-only parameter mismatch; parameter \"series\" is not position-only\n      Position-only parameter mismatch; parameter \"timeperiod\" is not position-only\n      Position-only parameter mismatch; expected 2 but received 0\n      Function return type \"Series[Any] | NDArray[floating[Any]]\" is incompatible with type \"Series[Any]\"\n        Type \"Series[Any] | NDArray[floating[Any]]\" is not assignable to type \"Series[Any]\"\n  ...",
       "rule": "reportAssignmentType",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 3932
+      "startLine": 4008
     },
     {
       "endCharacter": 35,
-      "endLine": 3978,
+      "endLine": 4054,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"to_numpy\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 27,
-      "startLine": 3978
+      "startLine": 4054
     },
     {
       "endCharacter": 33,
-      "endLine": 3979,
+      "endLine": 4055,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"to_numpy\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 25,
-      "startLine": 3979
+      "startLine": 4055
     },
     {
       "endCharacter": 37,
-      "endLine": 4096,
+      "endLine": 4172,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"to_numpy\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 29,
-      "startLine": 4096
+      "startLine": 4172
     },
     {
       "endCharacter": 35,
-      "endLine": 4097,
+      "endLine": 4173,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"to_numpy\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 27,
-      "startLine": 4097
+      "startLine": 4173
     },
     {
       "endCharacter": 33,
-      "endLine": 4098,
+      "endLine": 4174,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"to_numpy\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 25,
-      "startLine": 4098
+      "startLine": 4174
     },
     {
       "endCharacter": 39,
-      "endLine": 4118,
+      "endLine": 4194,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"to_numpy\" is not a known attribute of \"None\"",
       "rule": "reportOptionalMemberAccess",
       "severity": "error",
       "startCharacter": 31,
-      "startLine": 4118
+      "startLine": 4194
     },
     {
       "endCharacter": 92,
-      "endLine": 4477,
+      "endLine": 4553,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Type \"bool_ | bool\" is not assignable to return type \"bool\"\n  Type \"bool_ | bool\" is not assignable to type \"bool\"\n    \"numpy.bool[builtins.bool]\" is not assignable to \"bool\"",
       "rule": "reportReturnType",
       "severity": "error",
       "startCharacter": 15,
-      "startLine": 4477
+      "startLine": 4553
     },
     {
       "endCharacter": 85,
-      "endLine": 4878,
+      "endLine": 4954,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Argument of type \"XGBoostPruningCallback\" cannot be assigned to parameter \"object\" of type \"RegressorCallback\" in function \"append\"\n  Type \"XGBoostPruningCallback\" is not assignable to type \"RegressorCallback\"\n    Type \"XGBoostPruningCallback\" is not assignable to type \"(...) -> Any\"\n    \"XGBoostPruningCallback\" is not assignable to \"TrainingCallback\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 16,
-      "startLine": 4878
+      "startLine": 4954
     },
     {
       "endCharacter": 29,
-      "endLine": 4921,
+      "endLine": 4997,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Argument of type \"list[tuple[DataFrame, DataFrame]] | None\" cannot be assigned to parameter \"eval_set\" of type \"List[_LGBM_ScikitValidSet] | None\" in function \"fit\"\n  Type \"list[tuple[DataFrame, DataFrame]] | None\" is not assignable to type \"List[_LGBM_ScikitValidSet] | None\"\n    Type \"list[tuple[DataFrame, DataFrame]]\" is not assignable to type \"List[_LGBM_ScikitValidSet] | None\"\n      \"list[tuple[DataFrame, DataFrame]]\" is not assignable to \"List[_LGBM_ScikitValidSet]\"\n        Type parameter \"_T@list\" is invariant, but \"tuple[DataFrame, DataFrame]\" is not the same as \"_LGBM_ScikitValidSet\"\n        Consider switching from \"list\" to \"Sequence\" which is covariant\n      \"list[tuple[DataFrame, DataFrame]]\" is not assignable to \"None\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 21,
-      "startLine": 4921
+      "startLine": 4997
     },
     {
       "endCharacter": 43,
-      "endLine": 4922,
+      "endLine": 4998,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Argument of type \"list[NDArray[floating[Any]]] | None\" cannot be assigned to parameter \"eval_sample_weight\" of type \"List[_LGBM_WeightType] | None\" in function \"fit\"\n  Type \"list[NDArray[floating[Any]]] | None\" is not assignable to type \"List[_LGBM_WeightType] | None\"\n    Type \"list[NDArray[floating[Any]]]\" is not assignable to type \"List[_LGBM_WeightType] | None\"\n      \"list[NDArray[floating[Any]]]\" is not assignable to \"List[_LGBM_WeightType]\"\n        Type parameter \"_T@list\" is invariant, but \"NDArray[floating[Any]]\" is not the same as \"_LGBM_WeightType\"\n        Consider switching from \"list\" to \"Sequence\" which is covariant\n      \"list[NDArray[floating[Any]]]\" is not assignable to \"None\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 31,
-      "startLine": 4922
+      "startLine": 4998
     },
     {
       "endCharacter": 62,
-      "endLine": 4925,
+      "endLine": 5001,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Argument of type \"list[RegressorCallback] | None\" cannot be assigned to parameter \"callbacks\" of type \"List[(...) -> Unknown] | None\" in function \"fit\"\n  Type \"list[RegressorCallback] | None\" is not assignable to type \"List[(...) -> Unknown] | None\"\n    Type \"list[RegressorCallback]\" is not assignable to type \"List[(...) -> Unknown] | None\"\n      \"list[RegressorCallback]\" is not assignable to \"List[(...) -> Unknown]\"\n        Type parameter \"_T@list\" is invariant, but \"RegressorCallback\" is not the same as \"(...) -> Unknown\"\n        Consider switching from \"list\" to \"Sequence\" which is covariant\n      \"list[RegressorCallback]\" is not assignable to \"None\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 22,
-      "startLine": 4925
+      "startLine": 5001
     },
     {
       "endCharacter": 35,
-      "endLine": 4947,
+      "endLine": 5023,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Object of type \"None\" is not subscriptable",
       "rule": "reportOptionalSubscript",
       "severity": "error",
       "startCharacter": 27,
-      "startLine": 4947
+      "startLine": 5023
     },
     {
       "endCharacter": 35,
-      "endLine": 4993,
+      "endLine": 5069,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Object of type \"None\" is not subscriptable",
       "rule": "reportOptionalSubscript",
       "severity": "error",
       "startCharacter": 27,
-      "startLine": 4993
+      "startLine": 5069
     },
     {
       "endCharacter": 49,
-      "endLine": 5063,
+      "endLine": 5139,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Argument of type \"CatBoostPruningCallback\" cannot be assigned to parameter \"object\" of type \"RegressorCallback\" in function \"append\"\n  Type \"CatBoostPruningCallback\" is not assignable to type \"RegressorCallback\"\n    Type \"CatBoostPruningCallback\" is not assignable to type \"(...) -> Any\"\n    \"CatBoostPruningCallback\" is not assignable to \"TrainingCallback\"",
       "rule": "reportArgumentType",
       "severity": "error",
       "startCharacter": 33,
-      "startLine": 5063
+      "startLine": 5139
     },
     {
       "endCharacter": 75,
-      "endLine": 5306,
+      "endLine": 5382,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Operator \"<=\" not supported for \"None\"",
       "rule": "reportOptionalOperand",
       "severity": "error",
       "startCharacter": 54,
-      "startLine": 5306
+      "startLine": 5382
     },
     {
       "endCharacter": 23,
-      "endLine": 5469,
+      "endLine": 5545,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"best_params\" is possibly unbound",
       "rule": "reportPossiblyUnboundVariable",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 5469
+      "startLine": 5545
     },
     {
       "endCharacter": 38,
-      "endLine": 5476,
+      "endLine": 5552,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"best_params\" is possibly unbound",
       "rule": "reportPossiblyUnboundVariable",
       "severity": "error",
       "startCharacter": 27,
-      "startLine": 5476
+      "startLine": 5552
     },
     {
       "endCharacter": 22,
-      "endLine": 5488,
+      "endLine": 5564,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "\"best_params\" is possibly unbound",
       "rule": "reportPossiblyUnboundVariable",
       "severity": "error",
       "startCharacter": 11,
-      "startLine": 5488
+      "startLine": 5564
     },
     {
       "endCharacter": 22,
-      "endLine": 5646,
+      "endLine": 5722,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Declaration \"default_ranges\" is obscured by a declaration of the same name",
       "rule": "reportRedeclaration",
       "severity": "error",
       "startCharacter": 8,
-      "startLine": 5646
+      "startLine": 5722
     },
     {
       "endCharacter": 14,
-      "endLine": 5682,
+      "endLine": 5758,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Declaration \"params\" is obscured by a declaration of the same name",
       "rule": "reportRedeclaration",
       "severity": "error",
       "startCharacter": 8,
-      "startLine": 5682
+      "startLine": 5758
     },
     {
       "endCharacter": 22,
-      "endLine": 5766,
+      "endLine": 5842,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Declaration \"default_ranges\" is obscured by a declaration of the same name",
       "rule": "reportRedeclaration",
       "severity": "error",
       "startCharacter": 8,
-      "startLine": 5766
+      "startLine": 5842
     },
     {
       "endCharacter": 14,
-      "endLine": 5800,
+      "endLine": 5876,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Declaration \"params\" is obscured by a declaration of the same name",
       "rule": "reportRedeclaration",
       "severity": "error",
       "startCharacter": 8,
-      "startLine": 5800
+      "startLine": 5876
     },
     {
       "endCharacter": 22,
-      "endLine": 5871,
+      "endLine": 5947,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Declaration \"default_ranges\" is obscured by a declaration of the same name",
       "rule": "reportRedeclaration",
       "severity": "error",
       "startCharacter": 8,
-      "startLine": 5871
+      "startLine": 5947
     },
     {
       "endCharacter": 22,
-      "endLine": 5972,
+      "endLine": 6048,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Declaration \"default_ranges\" is obscured by a declaration of the same name",
       "rule": "reportRedeclaration",
       "severity": "error",
       "startCharacter": 8,
-      "startLine": 5972
+      "startLine": 6048
     },
     {
       "endCharacter": 26,
-      "endLine": 6055,
+      "endLine": 6131,
       "file": "quickadapter/user_data/strategies/Utils.py",
       "message": "Declaration \"default_ranges\" is obscured by a declaration of the same name",
       "rule": "reportRedeclaration",
       "severity": "error",
       "startCharacter": 12,
-      "startLine": 6055
+      "startLine": 6131
     }
   ],
   "filesAnalyzed": 5,
index 8d13f9bda118d819ce736d727237476c202fe1fb..cf76ca146a453175bddad4b0f0c69762c5a89c7d 100644 (file)
@@ -201,10 +201,11 @@ and the [FreqAI parameter reference][freqai-parameters].
 
 ### Exit pricing
 
-| Path                                                | Runtime fallback | Type / Range                                                        | Description                                                                                                                                                                                                                                                                                                                                                                                                                 |
-| --------------------------------------------------- | ---------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| exit_pricing.trade_natr_method                      | `moving_average` | enum {`moving_average`,`quantile_interpolation`,`weighted_average`} | Trade NATR (Normalized Average True Range) aggregation for stoploss and take-profit distances. `moving_average` uses KAMA to preserve nonnegative volatility. The stoploss never loosens, including after partial exits, and remains unchanged on order fills.                                                                                                                                                              |
-| exit_pricing.final_take_profit_retracement_fraction | 0.25             | float (0,1]                                                         | Fraction of the final take-profit target distance used as the frozen trailing retracement distance after the final target arms the exit. The final exit tracks the best subsequent per-candle rate and exits only after this material adverse move; elapsed stagnation alone does not exit. Plot annotations show only the current trail boundary from the candle that established it; earlier boundaries are not retained. |
+| Path                                                           | Runtime fallback       | Type / Range                                                        | Description                                                                                                                                                                                                                                                                                                                                                                                                                 |
+| -------------------------------------------------------------- | ---------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| exit_pricing.trade_natr_method                                 | `moving_average`       | enum {`moving_average`,`quantile_interpolation`,`weighted_average`} | Trade NATR (Normalized Average True Range) aggregation for stoploss and take-profit distances. `moving_average` uses KAMA to preserve nonnegative volatility. The stoploss never loosens, including after partial exits, and remains unchanged on order fills.                                                                                                                                                              |
+| exit_pricing.final_take_profit_retracement_fraction            | 0.5                    | float (0,1]                                                         | Fraction of the final take-profit target distance used as the frozen trailing retracement distance after the final target arms the exit. The final exit tracks the best subsequent per-candle rate and exits only after this material adverse move; elapsed stagnation alone does not exit. Plot annotations show only the current trail boundary from the candle that established it; earlier boundaries are not retained. |
+| exit_pricing.take_profit_stage_natr_multiplier_fraction_series | `fibonacci_extensions` | enum {`fibonacci_extensions`,`pi_extensions`}                       | Partial target distances from entry, as percentages of the full target distance: approximately 23.6%, 38.2% and 61.8% for `fibonacci_extensions`, or 3.2%, 10.1% and 31.8% for `pi_extensions`. Pi places partial targets closer to entry. Stake reduction percentages and the final target are unchanged. Invalid values use the default.                                                                                  |
 
 ### Reversal confirmation
 
@@ -220,7 +221,7 @@ and the [FreqAI parameter reference][freqai-parameters].
 | Path                                | Runtime fallback | Type / Range                                                                     | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
 | ----------------------------------- | ---------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | freqai.fit_live_predictions_candles | 100              | int >= 1                                                                         | Required real-prediction calibration window per pair; also strategy startup candles and protection lookback basis. The template explicitly supplies 864. See [prediction warmup](#prediction-warmup-and-restarts).                                                                                                                                                                                                                                                                                                                                                                                                                 |
-| freqai.regressor                    | `xgboost`        | enum {`xgboost`,`lightgbm`,`histgradientboostingregressor`,`ngboost`,`catboost`} | Machine learning regressor. With validation and early stopping enabled, NGBoost uses the iteration with the best validation score for prediction and to determine the final refit size.                                                                                                                                                                                                                                                                                                                                                                                                                                            |
+| freqai.regressor                    | `xgboost`        | enum {`xgboost`,`lightgbm`,`histgradientboostingregressor`,`ngboost`,`catboost`} | Machine learning regressor. Unknown names warn and fall back to `xgboost`. With validation and early stopping enabled, NGBoost uses the iteration with the best validation score for prediction and to determine the final refit size.                                                                                                                                                                                                                                                                                                                                                                                             |
 | freqai.continual_learning           | false            | bool                                                                             | Continue training the deployed XGBoost, LightGBM or CPU CatBoost model. GPU CatBoost and other regressors train from scratch. With two-stage selection (see `test_size`), HPO and selection train from scratch; only the final refit continues the deployed model, adding the selected number of boosting rounds. Continued training reuses the saved feature and label transformations, including after restart. Missing or incompatible transformations or deployment metadata prevent continuation. Reset trained models or use a new `freqai.identifier` to change pipeline settings or recover from incompatible saved state. |
 
 ### Model training parameters
@@ -288,7 +289,7 @@ and the [FreqAI parameter reference][freqai-parameters].
 | Path                                                           | Runtime fallback         | Type / Range                                                                                                                                                                                                 | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
 | -------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | freqai.feature_parameters.label_period_candles                 | min/max midpoint         | int >= 1                                                                                                                                                                                                     | Zigzag labeling NATR period.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
-| freqai.feature_parameters.label_horizon_candles                | `label_period_candles`   | int >= 1                                                                                                                                                                                                     | Conservative fixed purge horizon in candles: the magnitude of the causal guards' purge and of the default `timeseries_split` gap (see `causal_mode` for how the guards consume it). When unset, falls back to `label_period_candles`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
+| freqai.feature_parameters.label_horizon_candles                | `label_period_candles`   | int >= 1                                                                                                                                                                                                     | Conservative fixed purge horizon in candles: the magnitude of the causal guards' purge and of the default `timeseries_split` gap (see `causal_mode` for how the guards consume it). When unset or invalid, falls back to each pair's `label_period_candles`. …                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
 | freqai.feature_parameters.causal_mode                          | true                     | bool                                                                                                                                                                                                         | Causal split-guard master toggle. When `true` (default): (1) rejects `data_split_parameters.shuffle=true`, `feature_parameters.shuffle_after_split=true`, and `feature_parameters.reverse_train_test_order=true` (two of these rejections are independent of this toggle: `timeseries_split` rejects `shuffle_after_split` structurally, and an active holdout `test_size != 0` rejects all three at evaluation); (2) for `timeseries_split`, auto-sets `gap=label_horizon_candles` when `gap` is unset or `0` and rejects an explicit `gap<label_horizon_candles`; (3) for `train_test_split`, applies the same fixed `label_horizon_candles` purge around the train/test boundary; (4) both split methods additionally drop any train row whose label-aware availability reaches the test boundary; (5) label weighting becomes causal: the `epsilon` baseline at each row uses only pivot weights available with that row's label. `false` is deprecated: the causal split-guard rejections are lifted, but the toggle-independent ones remain (`timeseries_split` still rejects `shuffle_after_split`, and an active holdout still rejects all three at evaluation); `timeseries_split` `gap` auto-sets from `label_period_candles`, and Gaussian fills keep unbounded tails. |
 | freqai.feature_parameters.min_label_period_candles             | 12                       | int >= 1                                                                                                                                                                                                     | Minimum labeling NATR period used for reversals labeling HPO.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
 | freqai.feature_parameters.max_label_period_candles             | 24                       | int >= 1                                                                                                                                                                                                     | Maximum labeling NATR period used for reversals labeling HPO.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
@@ -325,7 +326,7 @@ and the [FreqAI parameter reference][freqai-parameters].
 
 | Path                                                        | Runtime fallback | Type / Range                           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
 | ----------------------------------------------------------- | ---------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| freqai.optuna_hyperopt.enabled                              | false            | bool                                   | Enables regressor and dynamic label HPO.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
+| freqai.optuna_hyperopt.enabled                              | false            | bool                                   | Enables regressor and dynamic label HPO. Active only when FreqAI is enabled and `test_size` is not 0.                                                                                                                                                                                                                                                                                                                                                                                                                                              |
 | freqai.optuna_hyperopt.sampler                              | `tpe`            | enum {`tpe`,`auto`}                    | HPO sampler algorithm for `hp` namespace. `tpe` uses [TPESampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.TPESampler.html) with multivariate, group, and constant_liar (when multiple workers), `auto` uses [AutoSampler](https://hub.optuna.org/samplers/auto_sampler).                                                                                                                                                                                                                              |
 | freqai.optuna_hyperopt.label_sampler                        | `auto`           | enum {`auto`,`tpe`,`nsgaii`,`nsgaiii`} | HPO sampler algorithm for multi-objective `label` namespace. `nsgaii` uses [NSGAIISampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.NSGAIISampler.html), `nsgaiii` uses [NSGAIIISampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.NSGAIIISampler.html).                                                                                                                                                                                                    |
 | freqai.optuna_hyperopt.storage                              | `file`           | enum {`file`,`sqlite`}                 | HPO storage backend.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
index 4d0bff06fd9515cfb56c52e06cfbc67feb2f3a44..2a0d894f20c3333340435c0b44d3e61a2dc06148 100644 (file)
@@ -13,6 +13,7 @@ from unittest import mock
 import numpy as np
 import pandas as pd
 import QuickAdapterV3 as strategy_module
+from freqtrade.enums import RunMode
 from LabelTransformer import (
     CUSTOM_THRESHOLD_METHODS,
     PREDICTION_METHODS,
@@ -20,7 +21,12 @@ from LabelTransformer import (
 )
 from qa_support import PAIR, QaTestCase, model_config, temporary_directory
 from QuickAdapterV3 import QuickAdapterV3
-from Utils import _OPTUNA_LABEL_SELECTION_SCHEMA_VERSION, _OPTUNA_NAMESPACES, enum_error_message
+from Utils import (
+    _OPTUNA_LABEL_SELECTION_SCHEMA_VERSION,
+    _OPTUNA_NAMESPACES,
+    DEFAULT_REGRESSOR,
+    REGRESSORS,
+)
 
 import quickadapter.user_data.freqaimodels.QuickAdapterRegressorV3 as regressor_module
 from quickadapter.user_data.freqaimodels.QuickAdapterRegressorV3 import (
@@ -115,13 +121,133 @@ def json_roundtrip(metadata: dict) -> object:
 
 
 class RegressorLabelConfigTest(QaTestCase):
-    def test_unknown_label_method_raises_with_the_canonical_enum_message(self):
-        with self.assertRaises(ValueError) as caught:
+    def test_unknown_regressor_warns_before_preserving_the_canonical_fallback(self):
+        rejected = "unknown-regressor"
+        with temporary_directory() as temp:
+            config = model_config(
+                temp,
+                freqai={"regressor": rejected, "label_prediction": {"method": "thresholding"}},
+            )
+            with self.assertLogs(REGRESSOR_LOGGER, level="WARNING") as logs:
+                model = QuickAdapterRegressorV3(config=config)
+            self.assertEqual(model.regressor, DEFAULT_REGRESSOR)
+            self.assertEqual(config["freqai"]["regressor"], DEFAULT_REGRESSOR)
+            warning = "\n".join(record.getMessage() for record in logs.records)
+            for value in (rejected, DEFAULT_REGRESSOR, *REGRESSORS):
+                self.assertIn(value, warning)
+
+    def test_startup_deduplicates_horizon_warnings_without_merging_pair_fallbacks(self):
+        pairs = [PAIR, "ETH/USDT", "SOL/USDT"]
+        for rejected in (0, True, None, [], {}):
+            with self.subTest(rejected=rejected), temporary_directory() as temp:
+                config = model_config(
+                    temp,
+                    exchange={"pair_whitelist": pairs},
+                    freqai={
+                        "optuna_hyperopt": {"enabled": False},
+                        "feature_parameters": {
+                            "label_period_candles": 7,
+                            "label_horizon_candles": rejected,
+                        },
+                        "label_prediction": {"method": "thresholding"},
+                    },
+                )
+                config["runmode"] = RunMode.BACKTEST
+                with self.assertLogs(REGRESSOR_LOGGER, level="INFO") as initial:
+                    model = QuickAdapterRegressorV3(config=config)
+                horizon_warnings = [
+                    record
+                    for record in initial.records
+                    if record.levelname == "WARNING"
+                    and "label_horizon_candles" in record.getMessage()
+                ]
+                self.assertEqual(1, len(horizon_warnings))
+                params = dict(model.get_optuna_params(pairs[1], _OPTUNA_NAMESPACES.label))
+                params["label_period_candles"] = 13
+                model.set_optuna_params(pairs[1], _OPTUNA_NAMESPACES.label, params)
+                for _ in range(2):
+                    with self.assertLogs(REGRESSOR_LOGGER, level="INFO") as startup:
+                        model._log_model_configuration()
+                    horizon_warnings = [
+                        record
+                        for record in startup.records
+                        if record.levelname == "WARNING"
+                        and "label_horizon_candles" in record.getMessage()
+                    ]
+                    self.assertEqual(2, len(horizon_warnings))
+                    messages = [record.getMessage() for record in startup.records]
+                    for pair, horizon in zip(pairs, (7, 13, 7), strict=True):
+                        pair_message = next(
+                            message for message in messages if f"{pair}:" in message
+                        )
+                        self.assertIn(f"label_horizon_candles={horizon}", pair_message)
+                with self.assertLogs(REGRESSOR_LOGGER, level="WARNING") as runtime:
+                    horizons = [model._label_horizon_candles(pair) for pair in pairs]
+                self.assertEqual([7, 13, 7], horizons)
+                self.assertEqual(3, len(runtime.records))
+
+    def test_startup_reports_requested_hpo_separately_from_effective_activation(self):
+        for requested, freqai_enabled, test_size, active in (
+            (False, True, 0.2, False),
+            (True, False, 0.2, False),
+            (True, True, 0, False),
+            (True, True, 0.2, True),
+        ):
+            with (
+                self.subTest(
+                    requested=requested, freqai_enabled=freqai_enabled, test_size=test_size
+                ),
+                temporary_directory() as temp,
+            ):
+                config = model_config(
+                    temp,
+                    freqai={
+                        "enabled": freqai_enabled,
+                        "optuna_hyperopt": {"enabled": requested},
+                        "data_split_parameters": {"test_size": test_size},
+                        "label_prediction": {"method": "thresholding"},
+                    },
+                )
+                with self.assertLogs(REGRESSOR_LOGGER, level="INFO") as logs:
+                    model = QuickAdapterRegressorV3(config=config)
+                self.assertEqual(model._optuna_hyperopt, active)
+                self.assertEqual(bool(model._optuna_label_candle), active)
+                fields = {
+                    key.strip(): value.strip()
+                    for record in logs.records
+                    for key, separator, value in [record.getMessage().partition(":")]
+                    if separator
+                }
+                self.assertEqual(fields["enabled"], str(requested))
+                self.assertEqual(fields["active"], str(active))
+                self.assertEqual("inactive_reason" in fields, not active)
+                self.assertEqual("n_trials" in fields, active)
+
+    def test_inactive_requested_hpo_preserves_configured_label_validation(self):
+        for feature_parameters, error in (
+            ({"label_method": "unknown-method"}, ValueError),
+            ({"label_p_order": "invalid-order"}, ValueError),
+            ({"label_weights": 7}, TypeError),
+        ):
+            with self.subTest(feature_parameters=feature_parameters), temporary_directory() as temp:
+                config = model_config(
+                    temp,
+                    freqai={
+                        "feature_parameters": feature_parameters,
+                        "data_split_parameters": {"test_size": 0},
+                        "label_prediction": {"method": "thresholding"},
+                    },
+                )
+                config["runmode"] = RunMode.BACKTEST
+                with self.assertRaises(error):
+                    QuickAdapterRegressorV3(config=config)
+                config["freqai"]["optuna_hyperopt"]["enabled"] = False
+                model = QuickAdapterRegressorV3(config=config)
+                self.assertFalse(model._optuna_hyperopt)
+
+    def test_unknown_label_method_is_rejected(self):
+        with self.assertRaises(ValueError):
             label_regressor()._resolve_label_method_config("kde")
-        self.assertEqual(
-            str(caught.exception),
-            enum_error_message("label_method", "kde", QuickAdapterRegressorV3._SELECTION_METHODS),
-        )
 
     def test_category_defaults_are_resolved_per_method(self):
         expected = {
@@ -176,43 +302,25 @@ class RegressorLabelConfigTest(QaTestCase):
                 with self.assertLogs(REGRESSOR_LOGGER, level="WARNING") as logs:
                     resolved = model._resolve_label_method_config(method)
                 self.assertEqual(resolved["distance_metric"], expected_default)
-                self.assertIn(f"Invalid {key} value {bad_metric!r}", "\n".join(logs.output))
+                self.assertIn(bad_metric, "\n".join(record.getMessage() for record in logs.records))
 
     def test_cluster_and_cluster_trial_selection_methods_are_fatal(self):
         for key in ("label_cluster_selection_method", "label_cluster_trial_selection_method"):
             with self.subTest(key=key):
                 model = label_regressor(ft_params={key: "kalman"})
-                with self.assertRaises(ValueError) as caught:
+                with self.assertRaises(ValueError):
                     model._resolve_label_method_config("kmeans")
-                self.assertEqual(
-                    str(caught.exception),
-                    enum_error_message(key, "kalman", QuickAdapterRegressorV3._DISTANCE_METHODS),
-                )
 
     def test_aggregate_metric_is_rejected_for_cluster_and_density_categories(self):
         aggregates = set(QuickAdapterRegressorV3._POWER_MEAN_MAP) | {"weighted_sum"}
         for aggregate in aggregates:
-            with self.subTest(aggregate=aggregate):
-                with self.assertRaises(ValueError) as caught:
-                    QuickAdapterRegressorV3._validate_label_selection_metric(
-                        aggregate,
-                        ctx="label_cluster_metric",
-                        default="euclidean",
-                        aggregate_allowed=False,
-                        mode="raise",
-                    )
-                self.assertEqual(
-                    str(caught.exception),
-                    enum_error_message(
-                        "label_cluster_metric",
-                        aggregate,
-                        tuple(
-                            candidate
-                            for candidate in QuickAdapterRegressorV3._DISTANCE_METRICS
-                            if candidate
-                            in QuickAdapterRegressorV3._CLUSTER_DENSITY_DISTANCE_METRICS_SET
-                        ),
-                    ),
+            with self.subTest(aggregate=aggregate), self.assertRaises(ValueError):
+                QuickAdapterRegressorV3._validate_label_selection_metric(
+                    aggregate,
+                    ctx="label_cluster_metric",
+                    default="euclidean",
+                    aggregate_allowed=False,
+                    mode="raise",
                 )
 
     def test_probability_metrics_are_rejected_for_cluster_and_density_categories(self):
@@ -603,18 +711,10 @@ class RegressorLabelConfigTest(QaTestCase):
             model.optuna_create_sampler("auto")
         load_module.assert_called_once_with("samplers/auto_sampler")
 
-    def test_optuna_create_sampler_reports_a_missing_sampler_through_the_enum_message(self):
+    def test_optuna_create_sampler_rejects_a_missing_sampler(self):
         model = label_regressor(_optuna_config=optuna_config(sampler=None))
-        with self.assertRaises(ValueError) as caught:
+        with self.assertRaises(ValueError):
             model.optuna_create_sampler(None)
-        self.assertEqual(
-            str(caught.exception),
-            enum_error_message(
-                "optuna sampler",
-                None,
-                QuickAdapterRegressorV3._OPTUNA_SAMPLERS,
-            ),
-        )
 
     def test_optuna_create_sampler_refuses_an_unmapped_sampler_by_exhaustiveness(self):
         model = label_regressor(_optuna_config=optuna_config())
@@ -646,12 +746,8 @@ class RegressorLabelConfigTest(QaTestCase):
 
     def test_optuna_samplers_by_namespace_refuses_an_unknown_namespace(self):
         model = label_regressor(_optuna_config=optuna_config())
-        with self.assertRaises(ValueError) as caught:
+        with self.assertRaises(ValueError):
             model.optuna_samplers_by_namespace("hp_label")
-        self.assertEqual(
-            str(caught.exception),
-            enum_error_message("namespace", "hp_label", _OPTUNA_NAMESPACES),
-        )
 
     def test_candle_pool_full_is_centred_on_the_label_frequency(self):
         for frequency, expected in (
index 0a199c28d0646719866a0a01a302585845fa6dff..cfea3d2ae0ccab36eca7fb4e0bd04494c854cee8 100644 (file)
@@ -3,6 +3,7 @@
 import datetime
 import math
 import unittest
+from itertools import pairwise
 from types import SimpleNamespace
 from unittest import mock
 
@@ -13,6 +14,10 @@ from EnumErrors import enum_error_message
 from numpy.testing import assert_allclose
 from qa_support import PAIR, QaTestCase
 from QuickAdapterV3 import QuickAdapterV3
+from Utils import (
+    TAKE_PROFIT_STAGE_NATR_MULTIPLIER_FRACTION_SERIES,
+    get_take_profit_stage_natr_multiplier_fractions,
+)
 
 LONG = QuickAdapterV3._TRADE_LONG
 SHORT = QuickAdapterV3._TRADE_SHORT
@@ -73,7 +78,7 @@ def candles(
     return frame
 
 
-def strategy(label_period_candles=4, label_natr_multiplier=MULTIPLIER):
+def strategy(label_period_candles=4, label_natr_multiplier=MULTIPLIER, fraction_series=None):
     """A candle-cache strategy whose label parameters come from the pair, not the frame."""
     model = object.__new__(QuickAdapterV3)
     model._candle_deviation_cache = {}
@@ -91,6 +96,10 @@ def strategy(label_period_candles=4, label_natr_multiplier=MULTIPLIER):
         "timeframe": TIMEFRAME,
         "exit_pricing": {"trade_natr_method": "quantile_interpolation"},
     }
+    if fraction_series is not None:
+        model.config["exit_pricing"]["take_profit_stage_natr_multiplier_fraction_series"] = (
+            fraction_series
+        )
     return model
 
 
@@ -529,10 +538,12 @@ class StrategyFeaturesTest(QaTestCase):
         long_trade = trade()
         # One candle of duration, a two-value trade NATR window and multiplier 3.0 make the
         # distance open_rate * 0.07 * 3.0 * fraction * log10(10), and log10(10) is exactly 1.
-        for stage, params in sorted(QuickAdapterV3.partial_exit_stages.items()):
+        for stage in sorted(QuickAdapterV3.partial_exit_stages):
             with self.subTest(stage=stage):
-                fraction = params[0]
+                fraction = model.partial_exit_stage_natr_multiplier_fractions[stage]
                 self.assertIsInstance(fraction, float)
+                self.assertGreater(fraction, 0.0)
+                self.assertLess(fraction, 1.0)
                 distance = 100.0 * 0.07 * MULTIPLIER * fraction
                 target = model.get_take_profit_target(frame, long_trade, stage)
                 self.assertIsNotNone(target)
@@ -553,6 +564,120 @@ class StrategyFeaturesTest(QaTestCase):
         ]
         self.assertEqual(stages, sorted(stages))
 
+    def test_the_fibonacci_extensions_define_the_default_partial_ladder(self):
+        # Frozen reference rungs, not a restatement of the formula: a change of
+        # base or of the stage count must fail here. They are phi**-3, phi**-2
+        # and phi**-1 rounded to six decimals.
+        assert_allclose(
+            tuple(strategy().partial_exit_stage_natr_multiplier_fractions.values()),
+            (0.236068, 0.381966, 0.618034),
+            rtol=1e-6,
+        )
+        self.assertEqual(strategy().get_exit_stage_natr_multiplier_fraction(99), 1.0)
+
+    def test_every_series_raises_its_fraction_with_the_stage_index(self):
+        for series in TAKE_PROFIT_STAGE_NATR_MULTIPLIER_FRACTION_SERIES:
+            with self.subTest(series=series):
+                # Stage fractions follow stage indices, not insertion order.
+                reordered_stages = dict(
+                    sorted(QuickAdapterV3.partial_exit_stages.items(), reverse=True)
+                )
+                with mock.patch.object(QuickAdapterV3, "partial_exit_stages", reordered_stages):
+                    model = strategy(fraction_series=series)
+                    by_stage = model.partial_exit_stage_natr_multiplier_fractions
+                    stages = sorted(QuickAdapterV3.partial_exit_stages)
+                    self.assertEqual(sorted(by_stage), stages)
+                    for earlier, later in pairwise(stages):
+                        self.assertLess(by_stage[earlier], by_stage[later])
+
+    def test_no_series_ladder_reaches_the_final_stage_fraction(self):
+        for series in TAKE_PROFIT_STAGE_NATR_MULTIPLIER_FRACTION_SERIES:
+            with self.subTest(series=series):
+                fractions = tuple(
+                    strategy(
+                        fraction_series=series
+                    ).partial_exit_stage_natr_multiplier_fractions.values()
+                )
+                self.assertNotIn(1.0, fractions)
+                self.assertEqual(max(fractions), fractions[-1])
+
+    def test_the_ladder_is_indexed_by_the_stage_count(self):
+        self.assertEqual(
+            len(get_take_profit_stage_natr_multiplier_fractions("fibonacci_extensions", 0)), 0
+        )
+        for count in (1, 2, 3, 4, 5):
+            with self.subTest(count=count):
+                fractions = get_take_profit_stage_natr_multiplier_fractions(
+                    "fibonacci_extensions", count
+                )
+                self.assertEqual(len(fractions), count)
+                self.assertEqual(max(fractions), fractions[-1])
+
+    def test_the_pi_extensions_define_the_partial_ladder(self):
+        assert_allclose(
+            tuple(
+                strategy(
+                    fraction_series="pi_extensions"
+                ).partial_exit_stage_natr_multiplier_fractions.values()
+            ),
+            (0.03225153, 0.10132118, 0.31830989),
+            rtol=1e-6,
+        )
+
+    def test_selecting_a_series_changes_the_stage_distances(self):
+        frame = candles([100.0, 100.0, 100.0], natr=[1.0, 4.0, 8.0], multiplier=MULTIPLIER)
+        long_trade = trade()
+        fibonacci = strategy(fraction_series="fibonacci_extensions")
+        pi = strategy(fraction_series="pi_extensions")
+        self.assertEqual(pi.take_profit_stage_natr_multiplier_fraction_series, "pi_extensions")
+        for stage in sorted(QuickAdapterV3.partial_exit_stages):
+            with self.subTest(stage=stage):
+                fibonacci_distance = fibonacci.get_take_profit_target(frame, long_trade, stage)[1]
+                pi_distance = pi.get_take_profit_target(frame, long_trade, stage)[1]
+                # The larger pi extension base yields smaller reciprocal fractions.
+                self.assertLess(pi_distance, fibonacci_distance)
+        # The final distance is independent of the partial extension series.
+        self.assertEqual(
+            fibonacci.get_take_profit_target(frame, long_trade, QuickAdapterV3._FINAL_EXIT_STAGE),
+            pi.get_take_profit_target(frame, long_trade, QuickAdapterV3._FINAL_EXIT_STAGE),
+        )
+
+    def test_fraction_series_validates_names_and_stage_counts(self):
+        with self.assertRaises(ValueError):
+            get_take_profit_stage_natr_multiplier_fractions("not_a_series", 3)
+        self.assertEqual(
+            get_take_profit_stage_natr_multiplier_fractions("fibonacci_extensions", 0), ()
+        )
+        assert_allclose(
+            get_take_profit_stage_natr_multiplier_fractions("fibonacci_extensions", 1),
+            (0.618033988749895,),
+            rtol=1e-14,
+        )
+
+    def test_a_degenerate_ladder_base_is_refused_by_name(self):
+        from Utils import _TAKE_PROFIT_STAGE_NATR_MULTIPLIER_FRACTION_BASES
+
+        for name, base in (("zero", 0.0), ("unit", 1.0), ("near", 1.0 + 2**-52)):
+            with self.subTest(base=base):
+                _TAKE_PROFIT_STAGE_NATR_MULTIPLIER_FRACTION_BASES[name] = base
+                self.addCleanup(_TAKE_PROFIT_STAGE_NATR_MULTIPLIER_FRACTION_BASES.pop, name, None)
+                self.addCleanup(get_take_profit_stage_natr_multiplier_fractions.cache_clear)
+                with self.assertRaises(ValueError) as raised:
+                    get_take_profit_stage_natr_multiplier_fractions(name, 3)
+                self.assertIn(name, str(raised.exception))
+
+    def test_an_underflowing_ladder_is_refused(self):
+        # pi**-650 is the smallest positive subnormal; pi**-651 underflows to 0.0.
+        fractions = get_take_profit_stage_natr_multiplier_fractions("pi_extensions", 650)
+        self.assertEqual(len(fractions), 650)
+        self.assertEqual(fractions[0], math.ulp(0.0))
+        with self.assertRaises(ValueError):
+            get_take_profit_stage_natr_multiplier_fractions("pi_extensions", 651)
+
+    def test_a_negative_partial_stage_count_is_rejected(self):
+        with self.assertRaises(ValueError):
+            get_take_profit_stage_natr_multiplier_fractions("fibonacci_extensions", -1)
+
     def test_a_short_take_profit_target_flips_the_sign(self):
         model = strategy()
         frame = candles([100.0, 100.0, 100.0], natr=[1.0, 4.0, 8.0], multiplier=MULTIPLIER)
index 0df66a1cfcbf799b16f8700530a76bf71aacdb34..d1ba224dd8becbcf4ea1ea0cd9988443d5da1f6d 100644 (file)
@@ -295,9 +295,13 @@ class RuntimeSizingTest(QaTestCase):
                 )
 
     def test_partial_exit_reduces_stake_after_the_real_target_is_crossed(self):
-        for short, favorable, unfavorable in ((False, 120.0, 105.0), (True, 80.0, 95.0)):
+        for short in (False, True):
             with self.subTest(short=short):
                 position = self.position(short)
+                target = self.model.get_take_profit_target(self.frame, position, 0)[0]
+                # Priced off the resolved target so the ladder remains a free parameter.
+                crossed = target + (-1.0 if short else 1.0)
+                below = position.open_rate
                 store = {}
                 # Isolate only database persistence, not target or sizing calculations.
                 with (
@@ -315,12 +319,12 @@ class RuntimeSizingTest(QaTestCase):
                     before = self.model.adjust_trade_position(
                         position,
                         self.now,
-                        favorable,
+                        crossed,
                         0.2,
                         1.0,
                         1000.0,
-                        favorable,
-                        unfavorable,
+                        crossed,
+                        below,
                         0.2,
                         0.2,
                     )
@@ -328,16 +332,16 @@ class RuntimeSizingTest(QaTestCase):
                     after = self.model.adjust_trade_position(
                         position,
                         self.now,
-                        unfavorable,
+                        below,
                         0.2,
                         1.0,
                         1000.0,
-                        unfavorable,
-                        favorable,
+                        below,
+                        crossed,
                         0.2,
                         0.2,
                     )
-                    stake_reduction = -100.0 * QuickAdapterV3.partial_exit_stages[0][1]
+                    stake_reduction = -100.0 * QuickAdapterV3.partial_exit_stages[0][0]
                     direction = "short" if short else "long"
                     self.assertEqual(after, (stake_reduction, f"take_profit_{direction}_0"))
 
index 99a3d07adceb0f5da17c1dca086e1cef4c6ad47f..3addb11daf71fd4594fa5b5e476be0e04c8dc534 100644 (file)
@@ -23,10 +23,7 @@ from Utils import (
     normalize_fit_live_predictions_config,
 )
 
-CANONICAL_EXIT_PRICING: dict[str, Any] = {
-    "trade_natr_method": "moving_average",
-    "final_take_profit_retracement_fraction": 0.25,
-}
+CANONICAL_EXIT_PRICING: dict[str, Any] = copy.deepcopy(DEFAULTS_EXIT_PRICING)
 CANONICAL_PROTECTIONS: dict[str, Any] = {
     "trade_duration_candles": 72,
     "lookback_period_fraction": 0.5,
@@ -111,6 +108,29 @@ class UtilsConfigResolutionTest(QaTestCase):
                 self.assertEqual(method, resolved["trade_natr_method"])
                 self.assertEqual([], sink)
 
+    def test_exit_pricing_unknown_fraction_series_falls_back_with_a_warning(self) -> None:
+        with (
+            recorded_warnings() as (logger, _),
+            self.assertLogs(logger, level=logging.WARNING) as captured,
+        ):
+            resolved = get_exit_pricing_config(
+                {"take_profit_stage_natr_multiplier_fraction_series": "nope"}, logger
+            )
+        self.assertEqual(CANONICAL_EXIT_PRICING, resolved)
+        self.assertEqual([logging.WARNING], [record.levelno for record in captured.records])
+
+    def test_exit_pricing_every_supported_fraction_series_is_preserved(self) -> None:
+        for series in ("fibonacci_extensions", "pi_extensions"):
+            with self.subTest(series=series):
+                with recorded_warnings() as (logger, sink):
+                    resolved = get_exit_pricing_config(
+                        {"take_profit_stage_natr_multiplier_fraction_series": series}, logger
+                    )
+                self.assertEqual(
+                    series, resolved["take_profit_stage_natr_multiplier_fraction_series"]
+                )
+                self.assertEqual([], sink)
+
     def test_exit_pricing_retracement_fraction_range_is_half_open_at_zero(self) -> None:
         with recorded_warnings() as (logger, sink):
             self.assertEqual(
@@ -123,18 +143,18 @@ class UtilsConfigResolutionTest(QaTestCase):
 
         for rejected in (0, 0.0, -0.5, None, True, "0.25"):
             with self.subTest(rejected=rejected):
-                with recorded_warnings() as (logger, sink):
+                with (
+                    recorded_warnings() as (logger, _),
+                    self.assertLogs(logger, level=logging.WARNING) as captured,
+                ):
                     resolved = get_exit_pricing_config(
                         {"final_take_profit_retracement_fraction": rejected}, logger
                     )
-                self.assertEqual(0.25, resolved["final_take_profit_retracement_fraction"])
                 self.assertEqual(
-                    [
-                        f"Invalid exit_pricing final_take_profit_retracement_fraction value "
-                        f"{rejected!r}: must be a finite number > 0 <= 1, using default 0.25"
-                    ],
-                    sink,
+                    DEFAULTS_EXIT_PRICING["final_take_profit_retracement_fraction"],
+                    resolved["final_take_profit_retracement_fraction"],
                 )
+                self.assertEqual([logging.WARNING], [record.levelno for record in captured.records])
 
     def test_exit_pricing_returns_a_fresh_dict_each_call(self) -> None:
         with recorded_warnings() as (logger, sink):
index 0dc22765e88a5155952938c8119b9d0f2dd7d3c6..5fd52202f8df3fa3686a3dc8c637722a7878ec87 100644 (file)
@@ -48,8 +48,10 @@ from LabelTransformer import (
     CUSTOM_THRESHOLD_METHODS,
     EXTREMA_SELECTION_METHODS,
     LABEL_WEIGHT_SUPPORT_POLICIES,
+    NORMALIZATION_TYPES,
     PREDICTION_METHODS,
     SKIMAGE_THRESHOLD_METHODS,
+    STANDARDIZATION_TYPES,
     ExtremaSelectionMethod,
     LabelTransformer,
     LabelWeightSupportPolicy,
@@ -450,7 +452,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
     https://github.com/sponsors/robcaulk
     """
 
-    version = "3.13.0-rc.13"
+    version = "3.13.0-rc.14"
 
     _CALIBRATION_START_KEY: Final[str] = "quickadapter_calibration_start"
     _DEPLOYMENT_COORDINATE_MARKER_KEY: Final[str] = "quickadapter_deployment_coordinates"
@@ -1630,11 +1632,14 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
     def _causal_mode(self) -> bool:
         return get_causal_mode(self.ft_params, logger)
 
-    def _label_horizon_candles(self, pair: str | None = None) -> int:
-        if pair is None:
-            return get_label_horizon_candles(self.ft_params, logger)
-        label_params = self.get_optuna_params(pair, _OPTUNA_NAMESPACES.label)
-        return get_label_horizon_candles({**self.ft_params, **label_params}, logger)
+    def _label_horizon_candles(
+        self, pair: str | None = None, *, warning_messages: set[str] | None = None
+    ) -> int:
+        config = self.ft_params
+        if pair is not None:
+            label_params = self.get_optuna_params(pair, _OPTUNA_NAMESPACES.label)
+            config = {**config, **label_params}
+        return get_label_horizon_candles(config, logger, warning_messages=warning_messages)
 
     @property
     def _optuna_label_candle_pool_full(self) -> list[int]:
@@ -1721,6 +1726,11 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
 
         self.regressor: Regressor = self.freqai_info.get("regressor", DEFAULT_REGRESSOR)
         if self.regressor not in set(REGRESSORS):
+            logger.warning(
+                "%s, using default %r",
+                enum_error_message("freqai.regressor", self.regressor, REGRESSORS),
+                DEFAULT_REGRESSOR,
+            )
             self.regressor = DEFAULT_REGRESSOR
             self.freqai_info["regressor"] = self.regressor
         self._log_model_configuration()
@@ -1733,10 +1743,89 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         logger.info(f"Model Version: {self.version}")
         logger.info(f"Regressor: {self.regressor}")
 
+        split_parameters = self.data_split_parameters
+        split_method = split_parameters.get(
+            "method", QuickAdapterRegressorV3.DATA_SPLIT_METHOD_DEFAULT
+        )
+        logger.info("Data Split:")
+        logger.info(f"  method: {split_method}")
+        if split_method == QuickAdapterRegressorV3._DATA_SPLIT_TIMESERIES:
+            test_size = split_parameters.get("test_size")
+            logger.info(
+                f"  test_size: {format_number(test_size) if test_size is not None else 'dynamic (dataset size and n_splits)'}"
+            )
+            logger.info(
+                f"  n_splits: {split_parameters.get('n_splits', QuickAdapterRegressorV3.TIMESERIES_N_SPLITS_DEFAULT)}"
+            )
+            gap = split_parameters.get("gap")
+            if gap is None:
+                gap = QuickAdapterRegressorV3.TIMESERIES_GAP_DEFAULT
+            if gap == 0:
+                gap_source = (
+                    "label_horizon_candles" if self._causal_mode else "label_period_candles"
+                )
+                logger.info(
+                    f"  gap: automatic from per-pair {gap_source} (deferred until training)"
+                )
+            else:
+                logger.info(f"  gap: {gap}")
+            logger.info(
+                f"  max_train_size: {split_parameters.get('max_train_size', QuickAdapterRegressorV3.TIMESERIES_MAX_TRAIN_SIZE_DEFAULT)}"
+            )
+        else:
+            logger.info(
+                f"  test_size: {format_number(split_parameters.get('test_size', QuickAdapterRegressorV3._TEST_SIZE))}"
+            )
+            for key in ("train_size", "random_state", "stratify"):
+                if key in split_parameters:
+                    logger.info(f"  {key}: {split_parameters[key]}")
+        logger.info(f"  validation_size: {format_number(self._get_validation_size())}")
+
+        logger.info("Causality and Ordering:")
+        logger.info(f"  causal_mode: {self._causal_mode}")
+        logger.info(f"  shuffle: {split_parameters.get('shuffle', False)}")
+        logger.info(f"  shuffle_after_split: {self.ft_params.get('shuffle_after_split', False)}")
+        logger.info(
+            f"  reverse_train_test_order: {self.ft_params.get('reverse_train_test_order', False)}"
+        )
+
+        logger.info("Training:")
+        logger.info(f"  continual_learning: {self.continual_learning}")
+        logger.info("Configured Model Training Overrides:")
+        logger.info("  These overrides are not final fitted, HPO-selected or refit parameters.")
+        logger.info(
+            f"  model_training_parameters: {format_dict(self.model_training_parameters, style=_FORMAT_STYLE_DICT)}"
+        )
+
         logger.info("Optuna Hyperopt:")
         optuna_config = self._optuna_config
         logger.info(f"  enabled: {optuna_config.get('enabled')}")
+        logger.info(f"  active: {bool(self._optuna_hyperopt)}")
+        label_method = self.ft_params.get(
+            "label_method", QuickAdapterRegressorV3.LABEL_METHOD_DEFAULT
+        )
+        label_config = {}
+        formatted_label_weights = None
+        label_p_order = None
+        label_p_order_config = self.ft_params.get("label_p_order")
+        # Preserve the configured-enabled validation boundary even when HPO is inactive.
         if optuna_config.get("enabled"):
+            label_config = self._resolve_label_method_config(label_method)
+            label_weights = self.ft_params.get("label_weights")
+            if label_weights is not None:
+                formatted_label_weights = [format_number(w) for w in label_weights]
+            if label_p_order_config is not None:
+                label_p_order = float(label_p_order_config)
+
+        if not self._optuna_hyperopt:
+            if not optuna_config.get("enabled"):
+                inactive_reason = "configuration disabled"
+            elif not self.freqai_info.get("enabled", False):
+                inactive_reason = "FreqAI disabled"
+            else:
+                inactive_reason = "zero validation/holdout size"
+            logger.info(f"  inactive_reason: {inactive_reason}")
+        else:
             logger.info(f"  n_jobs: {optuna_config.get('n_jobs')}")
             logger.info(f"  sampler: {optuna_config.get('sampler')}")
             logger.info(f"  storage: {optuna_config.get('storage')}")
@@ -1759,24 +1848,18 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
 
             logger.info(f"  label_sampler: {optuna_config.get('label_sampler')}")
             logger.info(f"  label_candles_step: {optuna_config.get('label_candles_step')}")
-            label_method = self.ft_params.get(
-                "label_method", QuickAdapterRegressorV3.LABEL_METHOD_DEFAULT
-            )
+
             logger.info(f"  label_method: {label_method}")
 
-            label_config = self._resolve_label_method_config(label_method)
             QuickAdapterRegressorV3._log_label_method_config(label_config)
 
-            label_weights = self.ft_params.get("label_weights")
-            if label_weights is not None:
-                formatted_label_weights = [format_number(w) for w in label_weights]
+            if formatted_label_weights is not None:
                 logger.info(f"  label_weights: [{', '.join(formatted_label_weights)}]")
             else:
                 logger.info("  label_weights: [1.0, ...] * n_objectives, l1 normalized (default)")
 
-            label_p_order_config = self.ft_params.get("label_p_order")
             if label_p_order_config is not None:
-                logger.info(f"  label_p_order: {format_number(float(label_p_order_config))}")
+                logger.info(f"  label_p_order: {format_number(label_p_order)}")
             else:
                 distance_metric = label_config["distance_metric"]
                 if distance_metric in {
@@ -1800,17 +1883,21 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             )
             logger.info("  Pipeline:")
             logger.info(f"    standardization: {col_pipeline['standardization']}")
-            logger.info(
-                f"    robust_quantiles: ({format_number(col_pipeline['robust_quantiles'][0])}, {format_number(col_pipeline['robust_quantiles'][1])})"
-            )
-            logger.info(
-                f"    mmad_scaling_factor: {format_number(col_pipeline['mmad_scaling_factor'])}"
-            )
+            if col_pipeline["standardization"] == STANDARDIZATION_TYPES[2]:  # "robust"
+                logger.info(
+                    f"    robust_quantiles: ({format_number(col_pipeline['robust_quantiles'][0])}, {format_number(col_pipeline['robust_quantiles'][1])})"
+                )
+            elif col_pipeline["standardization"] == STANDARDIZATION_TYPES[3]:  # "mmad"
+                logger.info(
+                    f"    mmad_scaling_factor: {format_number(col_pipeline['mmad_scaling_factor'])}"
+                )
             logger.info(f"    normalization: {col_pipeline['normalization']}")
-            logger.info(
-                f"    minmax_range: ({format_number(col_pipeline['minmax_range'][0])}, {format_number(col_pipeline['minmax_range'][1])})"
-            )
-            logger.info(f"    sigmoid_scale: {format_number(col_pipeline['sigmoid_scale'])}")
+            if col_pipeline["normalization"] == NORMALIZATION_TYPES[1]:  # "minmax"
+                logger.info(
+                    f"    minmax_range: ({format_number(col_pipeline['minmax_range'][0])}, {format_number(col_pipeline['minmax_range'][1])})"
+                )
+            elif col_pipeline["normalization"] == NORMALIZATION_TYPES[2]:  # "sigmoid"
+                logger.info(f"    sigmoid_scale: {format_number(col_pipeline['sigmoid_scale'])}")
             logger.info(f"    gamma: {format_number(col_pipeline['gamma'])}")
 
             col_prediction = get_label_column_config(
@@ -1818,15 +1905,22 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             )
             logger.info("  Prediction:")
             logger.info(f"    method: {col_prediction['method']}")
-            logger.info(f"    selection_method: {col_prediction['selection_method']}")
-            logger.info(f"    threshold_method: {col_prediction['threshold_method']}")
-            logger.info(
-                f"    outlier_quantile: {format_number(col_prediction['outlier_quantile'])}"
-            )
-            logger.info(
-                f"    soft_extremum_alpha: {format_number(col_prediction['soft_extremum_alpha'])}"
-            )
-            logger.info(f"    keep_fraction: {format_number(col_prediction['keep_fraction'])}")
+            if col_prediction["method"] == PREDICTION_METHODS[1]:  # "thresholding"
+                logger.info(f"    selection_method: {col_prediction['selection_method']}")
+                logger.info(f"    threshold_method: {col_prediction['threshold_method']}")
+                logger.info(
+                    f"    outlier_quantile: {format_number(col_prediction['outlier_quantile'])}"
+                )
+                if (
+                    col_prediction["threshold_method"] == CUSTOM_THRESHOLD_METHODS[1]
+                ):  # "soft_extremum"
+                    logger.info(
+                        f"    soft_extremum_alpha: {format_number(col_prediction['soft_extremum_alpha'])}"
+                    )
+                if col_prediction["selection_method"] in EXTREMA_SELECTION_METHODS[:2]:
+                    logger.info(
+                        f"    keep_fraction: {format_number(col_prediction['keep_fraction'])}"
+                    )
             if col_prediction["method"] == PREDICTION_METHODS[0]:  # "none"
                 logger.warning(
                     f"  Prediction method is 'none' for label [{label_col}]: "
@@ -1834,53 +1928,41 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                     f"entry signals based on them will never trigger."
                 )
 
-        default_label_period_candles, default_label_natr_multiplier = self._label_defaults
-        label_period_candles = self.ft_params.get(
-            "label_period_candles", default_label_period_candles
-        )
-        label_natr_multiplier = float(
-            self.ft_params.get("label_natr_multiplier", default_label_natr_multiplier)
-        )
-        logger.info("Label Hyperparameters:")
+        logger.info("Label Parameters:")
         logger.info(f"  fit_live_predictions_candles: {self._fit_live_predictions_candles}")
-        if self._optuna_hyperopt:
-            logger.info(f"  label_period_candles: {label_period_candles} (initial value)")
+        horizon_warning_messages: set[str] = set()
+        for pair in self.pairs:
+            params = self.get_optuna_params(pair, _OPTUNA_NAMESPACES.label)
+            horizon = self._label_horizon_candles(pair, warning_messages=horizon_warning_messages)
             logger.info(
-                f"  label_natr_multiplier: {format_number(label_natr_multiplier)} (initial value)"
+                f"  {pair}: label_period_candles={params.get('label_period_candles')}, "
+                f"label_natr_multiplier={format_number(params.get('label_natr_multiplier'))}, "
+                f"label_horizon_candles={horizon} (resolved at startup)"
             )
-        logger.info(f"  label_frequency_candles: {self._label_frequency_candles}")
-        logger.info(f"  min_label_period_candles: {self._min_label_period_candles}")
-        logger.info(f"  max_label_period_candles: {self._max_label_period_candles}")
-        logger.info(
-            f"  min_label_natr_multiplier: {format_number(self._min_label_natr_multiplier)}"
-        )
-        logger.info(
-            f"  max_label_natr_multiplier: {format_number(self._max_label_natr_multiplier)}"
-        )
 
+        label_frequency_candles = self._label_frequency_candles
         if self._optuna_hyperopt:
-            logger.info("Label Parameters:")
-            for pair in self.pairs:
-                params = self._optuna_label_params.get(pair, {})
-                if params:
-                    logger.info(
-                        f"  {pair}: label_period_candles={params.get('label_period_candles')}, "
-                        f"label_natr_multiplier={format_number(params.get('label_natr_multiplier'))}, "
-                        f"label_horizon_candles={self._label_horizon_candles(pair)}"
-                    )
-        else:
-            logger.info("Label Parameters:")
-            logger.info(f"  label_period_candles: {label_period_candles}")
-            logger.info(f"  label_natr_multiplier: {format_number(label_natr_multiplier)}")
-            logger.info(f"  label_horizon_candles: {self._label_horizon_candles()}")
+            logger.info("Label Hyperparameters:")
+            logger.info(f"  label_frequency_candles: {label_frequency_candles}")
+            logger.info(f"  min_label_period_candles: {self._min_label_period_candles}")
+            logger.info(f"  max_label_period_candles: {self._max_label_period_candles}")
+            logger.info(
+                f"  min_label_natr_multiplier: {format_number(self._min_label_natr_multiplier)}"
+            )
+            logger.info(
+                f"  max_label_natr_multiplier: {format_number(self._max_label_natr_multiplier)}"
+            )
 
         scaler = self.ft_params.get("scaler", QuickAdapterRegressorV3.SCALER_DEFAULT)
         feature_range = self.ft_params.get("range", QuickAdapterRegressorV3.RANGE_DEFAULT)
-        logger.info("Feature Parameters:")
-        logger.info(f"  scaler: {scaler}")
-        logger.info(
+        # Preserve range resolution independently of whether the selected scaler uses it.
+        formatted_feature_range = (
             f"  range: ({format_number(feature_range[0])}, {format_number(feature_range[1])})"
         )
+        logger.info("Feature Parameters:")
+        logger.info(f"  scaler: {scaler}")
+        if scaler == QuickAdapterRegressorV3.SCALER_DEFAULT:  # "minmax"
+            logger.info(formatted_feature_range)
 
         logger.info("=" * 60)
 
index 5388032e6a128f9c02b80735feca5ecc87d6fc3d..77e3eba48ec5ceab8c47fd44520a6e4e14820802 100644 (file)
@@ -27,6 +27,7 @@ from freqtrade.strategy.interface import IStrategy
 from freqtrade.util import FtPrecise
 from LabelTransformer import (
     COMBINED_AGGREGATIONS,
+    FILL_BANDWIDTHS,
     FILL_METHODS,
     SMOOTHING_METHOD_MODES,
     SMOOTHING_METHODS,
@@ -77,6 +78,7 @@ from Utils import (
     get_ma_fn,
     get_reversal_confirmation_config,
     get_smoothing_kernel_half_width,
+    get_take_profit_stage_natr_multiplier_fractions,
     is_finite_number,
     label_known_at_lookahead_column_name,
     label_weight_column_name,
@@ -218,7 +220,7 @@ class QuickAdapterV3(IStrategy):
     _ANNOTATION_LINE_OFFSET_CANDLES: Final[int] = 10
 
     def version(self) -> str:
-        return "3.13.0-rc.13"
+        return "3.13.0-rc.14"
 
     timeframe = "5m"
     timeframe_minutes = timeframe_to_minutes(timeframe)
@@ -228,11 +230,14 @@ class QuickAdapterV3(IStrategy):
 
     position_adjustment_enable = True
 
-    # {stage: (natr_multiplier_fraction, stake_percent, color)}
-    partial_exit_stages: ClassVar[dict[int, tuple[float, float, str]]] = {
-        0: (0.4858, 0.4, "lime"),
-        1: (0.6180, 0.3, "yellow"),
-        2: (0.7640, 0.2, "coral"),
+    # {stage: (stake_percent, color)}. The reciprocal-extension fractions are
+    # not literals here: they resolve from the configured
+    # ``exit_pricing.take_profit_stage_natr_multiplier_fraction_series`` ladder,
+    # which is their single source of truth. The final rung stays the literal.
+    partial_exit_stages: ClassVar[dict[int, tuple[float, str]]] = {
+        0: (0.4, "lime"),
+        1: (0.3, "yellow"),
+        2: (0.2, "coral"),
     }
 
     # (natr_multiplier_fraction, stake_percent, color)
@@ -245,6 +250,11 @@ class QuickAdapterV3(IStrategy):
     # Final full-exit stage, derived from the configured partial exits.
     _FINAL_EXIT_STAGE: Final[int] = max(partial_exit_stages.keys(), default=-1) + 1
 
+    # ``get_trade_exit_stage`` derives a stage from the number of filled take-profit
+    # exits, so every stage it can reach must exist here. A gap would price a
+    # missing stage at the final rung instead of arming it.
+    assert sorted(partial_exit_stages) == list(range(len(partial_exit_stages)))
+
     _TAKE_PROFIT_ORDER_TAG_PREFIX: Final[str] = "take_profit_"
     _FINAL_TAKE_PROFIT_STATE_KEY: Final[str] = "final_take_profit_state"
     _FINAL_TAKE_PROFIT_STATE_VERSION: Final[int] = 3
@@ -436,6 +446,26 @@ class QuickAdapterV3(IStrategy):
     def final_take_profit_retracement_fraction(self) -> float:
         return float(self.exit_pricing["final_take_profit_retracement_fraction"])
 
+    @property
+    def take_profit_stage_natr_multiplier_fraction_series(self) -> str:
+        return str(self.exit_pricing["take_profit_stage_natr_multiplier_fraction_series"])
+
+    @cached_property
+    def partial_exit_stage_natr_multiplier_fractions(self) -> dict[int, float]:
+        """Reciprocal-extension fractions of the full take-profit distance, by stage."""
+        fractions = get_take_profit_stage_natr_multiplier_fractions(
+            self.take_profit_stage_natr_multiplier_fraction_series,
+            len(QuickAdapterV3.partial_exit_stages),
+        )
+        return dict(zip(sorted(QuickAdapterV3.partial_exit_stages), fractions, strict=True))
+
+    def get_exit_stage_natr_multiplier_fraction(self, exit_stage: int) -> float:
+        """Return a partial reciprocal-extension fraction or the final fraction ``1.0``."""
+        fractions = self.partial_exit_stage_natr_multiplier_fractions
+        if exit_stage in fractions:
+            return fractions[exit_stage]
+        return QuickAdapterV3._FINAL_EXIT_STAGE_PARAMS[0]
+
     @cached_property
     def reversal_confirmation(self) -> dict[str, int | float]:
         return get_reversal_confirmation_config(self.config.get("reversal_confirmation"), logger)
@@ -511,12 +541,19 @@ class QuickAdapterV3(IStrategy):
         self._log_strategy_configuration()
 
     def _log_strategy_configuration(self) -> None:
+        label_weighting = self.label_weighting
+        label_smoothing = self.label_smoothing
+        reversal_confirmation = self.reversal_confirmation
+        trade_natr_method = self.trade_natr_method
+        final_take_profit_retracement_fraction = self.final_take_profit_retracement_fraction
+        fraction_series = self.take_profit_stage_natr_multiplier_fraction_series
+        partial_exit_stage_fractions = self.partial_exit_stage_natr_multiplier_fractions
+        protections = self.protections
+
         logger.info("=" * 60)
         logger.info("QuickAdapter Strategy Configuration")
         logger.info("=" * 60)
 
-        label_weighting = self.label_weighting
-        label_smoothing = self.label_smoothing
         for label_col in LABEL_COLUMNS:
             logger.info(f"Label [{label_col}]:")
 
@@ -525,62 +562,72 @@ class QuickAdapterV3(IStrategy):
             )
             logger.info("  Weighting:")
             logger.info(f"    strategy: {col_weighting['strategy']}")
-            logger.info(
-                f"    metric_coefficients: {format_dict(col_weighting['metric_coefficients'], style=_FORMAT_STYLE_DICT)}"
-            )
-            logger.info(f"    aggregation: {col_weighting['aggregation']}")
-            if col_weighting["aggregation"] == COMBINED_AGGREGATIONS[5]:  # "softmax"
-                logger.info(
-                    f"    softmax_temperature: {format_number(col_weighting['softmax_temperature'])}"
-                )
-            fill_method = col_weighting["fill_method"]
-            logger.info(f"    fill_method: {fill_method}")
-            if fill_method in (
-                QuickAdapterV3._FILL_EPSILON,
-                QuickAdapterV3._FILL_EPSILON_GAUSSIAN,
-            ):
-                logger.info(f"    fill_epsilon: {format_number(col_weighting['fill_epsilon'])}")
-                logger.info(f"    fill_epsilon_baseline: {col_weighting['fill_epsilon_baseline']}")
-            if fill_method in (
-                QuickAdapterV3._FILL_GAUSSIAN,
-                QuickAdapterV3._FILL_EPSILON_GAUSSIAN,
-            ):
-                logger.info(
-                    f"    fill_sigma_candles: {format_number(col_weighting['fill_sigma_candles'])}"
-                )
+            if col_weighting["strategy"] != QuickAdapterV3._WEIGHT_NONE:
+                if col_weighting["strategy"] == WEIGHT_STRATEGIES[8]:  # "combined"
+                    logger.info(
+                        f"    metric_coefficients: {format_dict(col_weighting['metric_coefficients'], style=_FORMAT_STYLE_DICT)}"
+                    )
+                    logger.info(f"    aggregation: {col_weighting['aggregation']}")
+                    if col_weighting["aggregation"] == COMBINED_AGGREGATIONS[5]:  # "softmax"
+                        logger.info(
+                            f"    softmax_temperature: {format_number(col_weighting['softmax_temperature'])}"
+                        )
+                fill_method = col_weighting["fill_method"]
+                logger.info(f"    fill_method: {fill_method}")
+                if fill_method in (
+                    QuickAdapterV3._FILL_EPSILON,
+                    QuickAdapterV3._FILL_EPSILON_GAUSSIAN,
+                ):
+                    logger.info(f"    fill_epsilon: {format_number(col_weighting['fill_epsilon'])}")
+                    logger.info(
+                        f"    fill_epsilon_baseline: {col_weighting['fill_epsilon_baseline']}"
+                    )
+                if fill_method in (
+                    QuickAdapterV3._FILL_GAUSSIAN,
+                    QuickAdapterV3._FILL_EPSILON_GAUSSIAN,
+                ):
+                    logger.info(
+                        f"    fill_sigma_candles: {format_number(col_weighting['fill_sigma_candles'])}"
+                    )
+                    logger.info(f"    fill_bandwidth: {col_weighting['fill_bandwidth']}")
+                    if col_weighting["fill_bandwidth"] == FILL_BANDWIDTHS[1]:  # "knn"
+                        logger.info(
+                            f"    fill_sigma_min_candles: {format_number(col_weighting['fill_sigma_min_candles'])}"
+                        )
+                        logger.info(
+                            f"    fill_bandwidth_neighbors: {col_weighting['fill_bandwidth_neighbors']}"
+                        )
+                        logger.info(
+                            f"    fill_bandwidth_alpha: {format_number(col_weighting['fill_bandwidth_alpha'])}"
+                        )
+                logger.info(f"    support_policy: {col_weighting['support_policy']}")
                 logger.info(
-                    f"    fill_sigma_min_candles: {format_number(col_weighting['fill_sigma_min_candles'])}"
+                    f"    min_pivot_equivalent_count: {col_weighting['min_pivot_equivalent_count']}"
                 )
-                logger.info(f"    fill_bandwidth: {col_weighting['fill_bandwidth']}")
                 logger.info(
-                    f"    fill_bandwidth_neighbors: {col_weighting['fill_bandwidth_neighbors']}"
+                    f"    min_positive_label_weight_fraction: {format_number(col_weighting['min_positive_label_weight_fraction'])}"
                 )
                 logger.info(
-                    f"    fill_bandwidth_alpha: {format_number(col_weighting['fill_bandwidth_alpha'])}"
+                    f"    min_effective_sample_size: {format_number(col_weighting['min_effective_sample_size'])}"
                 )
-            logger.info(f"    support_policy: {col_weighting['support_policy']}")
-            logger.info(
-                f"    min_pivot_equivalent_count: {col_weighting['min_pivot_equivalent_count']}"
-            )
-            logger.info(
-                f"    min_positive_label_weight_fraction: {format_number(col_weighting['min_positive_label_weight_fraction'])}"
-            )
-            logger.info(
-                f"    min_effective_sample_size: {format_number(col_weighting['min_effective_sample_size'])}"
-            )
 
             col_smoothing = get_label_column_config(
                 label_col, label_smoothing["default"], label_smoothing["columns"]
             )
             logger.info("  Smoothing:")
-            logger.info(f"    method: {col_smoothing['method']}")
-            logger.info(f"    window_candles: {col_smoothing['window_candles']}")
-            logger.info(f"    beta: {format_number(col_smoothing['beta'])}")
-            logger.info(f"    polyorder: {col_smoothing['polyorder']}")
-            logger.info(f"    mode: {col_smoothing['mode']}")
-            logger.info(f"    sigma: {format_number(col_smoothing['sigma'])}")
-
             method = col_smoothing["method"]
+            logger.info(f"    method: {method}")
+            if method != SMOOTHING_METHODS[0]:  # "none"
+                logger.info(f"    window_candles: {col_smoothing['window_candles']}")
+                if method in SMOOTHING_METHODS[2:4]:  # Kaiser families
+                    logger.info(f"    beta: {format_number(col_smoothing['beta'])}")
+                if method == QuickAdapterV3._SMOOTHING_SAVGOL:
+                    logger.info(f"    polyorder: {col_smoothing['polyorder']}")
+                if method in SMOOTHING_METHOD_MODES:
+                    logger.info(f"    mode: {col_smoothing['mode']}")
+                if method == SMOOTHING_METHODS[8]:  # "gaussian_filter1d"
+                    logger.info(f"    sigma: {format_number(col_smoothing['sigma'])}")
+
             if col_weighting["strategy"] != QuickAdapterV3._WEIGHT_NONE and (
                 method == QuickAdapterV3._SMOOTHING_SMM
                 or (method == QuickAdapterV3._SMOOTHING_SAVGOL and col_smoothing["polyorder"] >= 2)
@@ -599,24 +646,23 @@ class QuickAdapterV3(IStrategy):
 
         logger.info("Reversal Confirmation:")
         logger.info(
-            f"  lookback_period_candles: {self.reversal_confirmation['lookback_period_candles']}"
+            f"  lookback_period_candles: {reversal_confirmation['lookback_period_candles']}"
         )
+        logger.info(f"  decay_fraction: {format_number(reversal_confirmation['decay_fraction'])}")
         logger.info(
-            f"  decay_fraction: {format_number(self.reversal_confirmation['decay_fraction'])}"
+            f"  min_natr_multiplier_fraction: {format_number(reversal_confirmation['min_natr_multiplier_fraction'])}"
         )
         logger.info(
-            f"  min_natr_multiplier_fraction: {format_number(self.reversal_confirmation['min_natr_multiplier_fraction'])}"
-        )
-        logger.info(
-            f"  max_natr_multiplier_fraction: {format_number(self.reversal_confirmation['max_natr_multiplier_fraction'])}"
+            f"  max_natr_multiplier_fraction: {format_number(reversal_confirmation['max_natr_multiplier_fraction'])}"
         )
 
         logger.info("Exit Pricing:")
-        logger.info(f"  trade_natr_method: {self.trade_natr_method}")
+        logger.info(f"  trade_natr_method: {trade_natr_method}")
         logger.info(
             "  final_take_profit_retracement_fraction: "
-            f"{format_number(self.final_take_profit_retracement_fraction)}"
+            f"{format_number(final_take_profit_retracement_fraction)}"
         )
+        logger.info(f"  take_profit_stage_natr_multiplier_fraction_series: {fraction_series}")
 
         logger.info("Custom Stoploss:")
         logger.info(
@@ -625,21 +671,25 @@ class QuickAdapterV3(IStrategy):
 
         logger.info("Partial Take-Profit Stages:")
         for stage, (
-            natr_multiplier_fraction,
             stake_percent,
             color,
         ) in QuickAdapterV3.partial_exit_stages.items():
             logger.info(
-                f"  stage {stage}: natr_multiplier_fraction={format_number(natr_multiplier_fraction)}, stake_percent={format_number(stake_percent)}, color={color}"
+                f"  stage {stage}: natr_multiplier_fraction="
+                f"{format_number(partial_exit_stage_fractions[stage])}, "
+                f"stake_percent={format_number(stake_percent)}, color={color}"
             )
 
+        logger.info("Final Exit:")
         logger.info(
-            f"Final Exit: natr_multiplier_fraction={format_number(QuickAdapterV3._FINAL_EXIT_STAGE_PARAMS[0])}, stake_percent={format_number(QuickAdapterV3._FINAL_EXIT_STAGE_PARAMS[1])}, color={QuickAdapterV3._FINAL_EXIT_STAGE_PARAMS[2]}"
+            f"  natr_multiplier_fraction: {format_number(QuickAdapterV3._FINAL_EXIT_STAGE_PARAMS[0])}"
         )
+        logger.info(f"  stake_percent: {format_number(QuickAdapterV3._FINAL_EXIT_STAGE_PARAMS[1])}")
+        logger.info(f"  color: {QuickAdapterV3._FINAL_EXIT_STAGE_PARAMS[2]}")
 
         logger.info("Protections:")
-        if self.protections:
-            for protection in self.protections:
+        if protections:
+            for protection in protections:
                 method = protection.get("method", "Unknown")
                 protection_params = {k: v for k, v in protection.items() if k != "method"}
                 logger.info(
@@ -1453,11 +1503,7 @@ class QuickAdapterV3(IStrategy):
     def get_take_profit_target(
         self, df: DataFrame, trade: Trade, exit_stage: int
     ) -> tuple[float, float] | None:
-        natr_multiplier_fraction = (
-            QuickAdapterV3.partial_exit_stages[exit_stage][0]
-            if exit_stage in QuickAdapterV3.partial_exit_stages
-            else QuickAdapterV3._FINAL_EXIT_STAGE_PARAMS[0]
-        )
+        natr_multiplier_fraction = self.get_exit_stage_natr_multiplier_fraction(exit_stage)
         take_profit_distance = self.get_take_profit_distance(df, trade, natr_multiplier_fraction)
         if not is_finite_number(take_profit_distance) or take_profit_distance <= 0:
             return None
@@ -1880,7 +1926,7 @@ class QuickAdapterV3(IStrategy):
                 ),
             )
         if trade_partial_exit:
-            trade_stake_percent = QuickAdapterV3.partial_exit_stages[trade_exit_stage][1]
+            trade_stake_percent = QuickAdapterV3.partial_exit_stages[trade_exit_stage][0]
             trade_partial_stake_amount = trade_stake_percent * trade.stake_amount
             tag = (
                 f"{QuickAdapterV3._TAKE_PROFIT_ORDER_TAG_PREFIX}"
@@ -2550,7 +2596,7 @@ class QuickAdapterV3(IStrategy):
                     "end": end_date,
                     "y_start": partial_take_profit_price,
                     "y_end": partial_take_profit_price,
-                    "color": QuickAdapterV3.partial_exit_stages[take_profit_stage][2],
+                    "color": QuickAdapterV3.partial_exit_stages[take_profit_stage][1],
                     "line_style": "solid",
                     "width": 1,
                     "label": f"Partial Take-Profit Stage {take_profit_stage}",
index 82965d7a865b2bbb438376f845e7cddcb920f58f..a808e035072857ef0ac3bd2f691e5055ab47c9a1 100644 (file)
@@ -1041,6 +1041,70 @@ TRADE_NATR_METHODS: Final[tuple[TradeNatrMethod, ...]] = (
 )
 
 
+TakeProfitStageNatrMultiplierFractionSeries = Literal["fibonacci_extensions", "pi_extensions"]
+TAKE_PROFIT_STAGE_NATR_MULTIPLIER_FRACTION_SERIES: Final[
+    tuple[TakeProfitStageNatrMultiplierFractionSeries, ...]
+] = (
+    "fibonacci_extensions",
+    "pi_extensions",
+)
+
+# Extension bases: stage ``k`` of ``n`` uses the reciprocal of ``base ** (n - k)``.
+# These fractions place partial targets inside the full take-profit distance;
+# they do not extend it. Fibonacci uses integer powers of phi; pi uses integer
+# powers of pi as a custom geometric construction, not a trading convention.
+# The final fraction ``1.0`` is the separate ``_FINAL_EXIT_STAGE_PARAMS``
+# literal.
+_TAKE_PROFIT_STAGE_NATR_MULTIPLIER_FRACTION_BASES: Final[dict[str, float]] = {
+    "fibonacci_extensions": (1.0 + math.sqrt(5.0)) / 2.0,
+    "pi_extensions": math.pi,
+}
+
+# The closest partial fraction is 1 / base; a 1.5 floor keeps it at most 2/3.
+# This relative gap does not guarantee distinct prices after rounding.
+_TAKE_PROFIT_STAGE_NATR_MULTIPLIER_FRACTION_MIN_BASE: Final[float] = 1.5
+
+
+@lru_cache(maxsize=_CACHE_MAXSIZE_SMALL)
+def get_take_profit_stage_natr_multiplier_fractions(
+    series: str, partial_stage_count: int
+) -> tuple[float, ...]:
+    """Return ascending reciprocal-extension fractions for partial take-profit targets.
+
+    Stage ``k`` of ``n`` uses ``1 / base ** (n - k)``. Each fraction scales
+    the full take-profit distance and remains strictly below the independent
+    final fraction ``1.0``.
+    """
+    base = _TAKE_PROFIT_STAGE_NATR_MULTIPLIER_FRACTION_BASES.get(series)
+    if base is None:
+        raise ValueError(
+            enum_error_message(
+                "take_profit_stage_natr_multiplier_fraction_series",
+                series,
+                TAKE_PROFIT_STAGE_NATR_MULTIPLIER_FRACTION_SERIES,
+            )
+        )
+    if partial_stage_count < 0:
+        raise ValueError(f"Invalid partial_stage_count value {partial_stage_count}: must be >= 0")
+    if base < _TAKE_PROFIT_STAGE_NATR_MULTIPLIER_FRACTION_MIN_BASE:
+        raise ValueError(
+            f"Invalid base value {base!r} for series {series!r}: "
+            f"must be >= {_TAKE_PROFIT_STAGE_NATR_MULTIPLIER_FRACTION_MIN_BASE}"
+        )
+
+    fractions = tuple(
+        base ** -(partial_stage_count - stage) for stage in range(partial_stage_count)
+    )
+    if any(fraction <= 0.0 for fraction in fractions):
+        # Only reachable by denormal underflow: the base floor above guarantees
+        # ``base > 1``, so no rung can reach or exceed ``1.0``.
+        raise ValueError(
+            f"Invalid partial_stage_count value {partial_stage_count!r} for series {series!r}: "
+            "NATR multiplier fractions must be > 0 (underflow to zero)"
+        )
+    return fractions
+
+
 SPARSE_TRAINING_MASS_THRESHOLD: Final[float] = 0.05
 
 DEFAULT_FIT_LIVE_PREDICTIONS_CANDLES: Final[int] = 100
@@ -1440,7 +1504,10 @@ def get_label_prediction_config(
 
 DEFAULTS_EXIT_PRICING: Final[dict[str, Any]] = {
     "trade_natr_method": TRADE_NATR_METHODS[0],  # "moving_average"
-    "final_take_profit_retracement_fraction": 0.25,
+    "final_take_profit_retracement_fraction": 0.5,
+    "take_profit_stage_natr_multiplier_fraction_series": (
+        TAKE_PROFIT_STAGE_NATR_MULTIPLIER_FRACTION_SERIES[0]
+    ),
 }
 
 _EXIT_PRICING_SPECS: Final[dict[str, _ParamSpec]] = {
@@ -1449,6 +1516,9 @@ _EXIT_PRICING_SPECS: Final[dict[str, _ParamSpec]] = {
         _NumericValidator(min_value=0, max_value=1, min_exclusive=True),
         output_type=float,
     ),
+    "take_profit_stage_natr_multiplier_fraction_series": _ParamSpec(
+        _EnumValidator(TAKE_PROFIT_STAGE_NATR_MULTIPLIER_FRACTION_SERIES), output_type=str
+    ),
 }
 
 
@@ -1634,7 +1704,9 @@ def get_causal_mode(config: dict[str, Any], logger: Logger) -> bool:
     return causal_mode
 
 
-def get_label_horizon_candles(config: dict[str, Any], logger: Logger) -> int:
+def get_label_horizon_candles(
+    config: dict[str, Any], logger: Logger, *, warning_messages: set[str] | None = None
+) -> int:
     def _is_positive_int(value: Any) -> bool:
         return not isinstance(value, bool) and isinstance(value, (int, np.integer)) and value >= 1
 
@@ -1643,10 +1715,14 @@ def get_label_horizon_candles(config: dict[str, Any], logger: Logger) -> int:
         fallback = 1
     label_horizon_candles = config.get("label_horizon_candles", fallback)
     if not _is_positive_int(label_horizon_candles):
-        logger.warning(
+        message = (
             f"Invalid label_horizon_candles value {label_horizon_candles!r}: "
             f"must be int >= 1, using {fallback!r}"
         )
+        if warning_messages is None or message not in warning_messages:
+            logger.warning(message)
+            if warning_messages is not None:
+                warning_messages.add(message)
         return fallback
     return int(label_horizon_candles)