From d7fe32e18de32d18bfc278300ab2af6da1c16bfb Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Thu, 24 Sep 2026 01:57:08 +0200 Subject: [PATCH] fix(freqai): prevent future continuations and align RL trade accounting (#275) * fix(freqai): bound continuation and correct RL accounting * fix(reforcexy): preserve second-level continuation and fee extrema * docs: reconcile strategy guides and invariant ownership * docs: complete development table of contents * fix(reward): preserve profitable synthetic paths at high fees * style(docs): format Markdown with Prettier * fix: harden reward analysis and runtime regressions * docs: describe ReforceXY leverage bounds * refactor(ci): consolidate strategy QA jobs * docs: explain strategy PYTHONPATH in QA * fix(reforcexy): report marginal unmasked action probabilities * style(docs): apply prettier to coverage mapping * fix(reforcexy): clarify diagnostics and deduplicate leverage warnings * fix(ci): restore Ruff import grouping * chore(quickadapter): bump version to 3.13.0-rc.12 --- .github/workflows/quality.yml | 32 +- README.md | 55 +- ReforceXY/.basedpyright/diagnostics.json | 622 +++++++----------- ReforceXY/reward_space_analysis/README.md | 61 +- .../reward_space_analysis.py | 227 +++++-- .../reward_space_analysis/tests/README.md | 121 ++-- .../tests/api/test_api_helpers.py | 130 +++- .../tests/cli/test_cli_params_and_csv.py | 39 ++ .../tests/helpers/test_utilities.py | 23 + .../tests/pbrs/test_pbrs.py | 270 +++++++- .../tests/statistics/test_statistics.py | 57 ++ ReforceXY/tests/test_portfolio_returns.py | 76 ++- ReforceXY/tests/test_review_contracts.py | 66 +- ReforceXY/tests/test_training_observations.py | 112 ++++ ReforceXY/user_data/freqaimodels/ReforceXY.py | 30 +- .../user_data/strategies/RLAgentStrategy.py | 27 +- quickadapter/.basedpyright/diagnostics.json | 200 +++--- quickadapter/tests/test_prediction_history.py | 216 ++++++ .../freqaimodels/QuickAdapterRegressorV3.py | 25 +- .../user_data/strategies/QuickAdapterV3.py | 2 +- 20 files changed, 1652 insertions(+), 739 deletions(-) create mode 100644 quickadapter/tests/test_prediction_history.py diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 019114d..1f6fdef 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -31,8 +31,8 @@ jobs: - name: Check repository formatting run: uvx ruff@latest format --check . - type-check: - name: BasedPyright (${{ matrix.name }}) + strategy-qa: + name: QA (${{ matrix.name }}) runs-on: ubuntu-latest timeout-minutes: 30 strategy: @@ -63,29 +63,23 @@ jobs: "${{ matrix.image }}" \ /workspace/scripts/check_basedpyright.py \ --project "${{ matrix.project }}" - - reforcexy-tests: - name: ReforceXY tests - runs-on: ubuntu-latest - timeout-minutes: 30 - env: - REFORCEXY_QA_IMAGE: freqai-strategies-reforcexy-qa:ci - steps: - - name: Check out repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Build QA image - run: >- - docker build --pull --target qa - --tag "${REFORCEXY_QA_IMAGE}" - ReforceXY - name: Run runtime regressions run: | docker run --rm \ --mount "type=bind,src=${GITHUB_WORKSPACE},dst=/workspace,readonly" \ --workdir /workspace \ + --env PYTHONPATH=/workspace/${{ matrix.context }}/user_data/strategies \ --entrypoint python \ - "${REFORCEXY_QA_IMAGE}" \ - -m unittest discover -s ReforceXY/tests -v + "${{ matrix.image }}" \ + -m unittest discover -s "${{ matrix.context }}/tests" -v + + reward-space-analysis: + name: Reward space analysis + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up uv uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: diff --git a/README.md b/README.md index 35d248f..b194c3c 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,20 @@ - [QuickAdapter](#quickadapter) - [Quick start](#quick-start) - [Configuration tunables](#configuration-tunables) + - [Continual learning](#continual-learning) + - [Live inference](#live-inference) - [Backtest evaluation protocol](#backtest-evaluation-protocol) - [ReforceXY](#reforcexy) - [Quick start](#quick-start-1) - [Supported models](#supported-models) - [Configuration tunables](#configuration-tunables-1) + - [Continual learning](#continual-learning-1) + - [Live inference](#live-inference-1) + - [Training and HPO](#training-and-hpo) + - [Reward and portfolio accounting](#reward-and-portfolio-accounting) - [Development](#development) + - [Runtime regressions](#runtime-regressions) + - [Quality checks](#quality-checks) - [Common workflows](#common-workflows) - [Note](#note) @@ -169,6 +177,16 @@ ignored with a warning. Matching column patterns are applied from least to most specific; equally specific patterns follow declaration order, so the later one wins. +### Continual learning + +In backtests, continual training uses only a saved model whose training cutoff +precedes the current window's end and its last available candle boundary. A +later model left under the same identifier is not reused when a backtest is +extended into the past. Live and dry-run restarts still restore compatible +deployed models. + +### Live inference + In live and dry-run modes, each pair requires `freqai.fit_live_predictions_candles` real model predictions before adaptive thresholds become available. The Nth observation first affects the next @@ -358,6 +376,10 @@ PPO, MaskablePPO, RecurrentPPO, DQN, QRDQN The documented list of model tunables is at the top of the [ReforceXY.py](./ReforceXY/user_data/freqaimodels/ReforceXY.py) file. +`RLAgentStrategy` clamps configured leverage to `[1, max_leverage]`. An omitted, +non-finite, non-representable, Boolean or non-numeric value falls back to +Freqtrade's `proposed_leverage` before clamping. + ### Continual learning Continual learning trains an independent copy of the deployed policy with its @@ -369,6 +391,12 @@ deployments without the chronological training marker. Training disables `shuffle_after_split`. HPO studies and saved best parameters are reused only when their objective identity matches. +Backtests continue only from a saved policy whose training cutoff precedes +the current window's end and its last available candle boundary. A later +deployment under the same identifier is not reused for an earlier window, +even when FreqAI has saved only metadata for intervening windows. Live and +dry-run restarts still restore compatible deployed policies. + ### Live inference Optional `fit_live_predictions_candles` statistics use the latest persisted real @@ -430,23 +458,36 @@ delta over the returned next observation. Termination liquidates any remaining position once and clears the terminal potential. `get_env_history()` returns one metrics/price row per transition. Its `execution_tick` is the transition/action/fill key before the tick increment; its `tick` is the returned post-increment price and -observation row (normally `execution_tick + 1`). Ordered trade events remain -separate in `trade_history`, where each event's `tick` equals the history row's -`execution_tick`; multiple events may share that key. `terminal_liquidation` and -`exit_pnl` remain on the transition history row. +observation row (normally `execution_tick + 1`). Exit-efficiency extrema include +the fee-adjusted PnL at entry and subsequent retained market marks. Ordered +trade events remain separate in `trade_history`: their tick identifies the +candle whose price filled the event. Action fills use `execution_tick`; +terminal liquidations use the returned post-increment tick. `terminal_liquidation` +and `exit_pnl` remain on the transition history row. ## Development ### Runtime regressions -Run the runtime training, inference and accounting regressions inside the -ReforceXY QA image, with the repository mounted at `/workspace` and `/workspace` -as the working directory: +Run each suite in its matching Freqtrade QA image, with the repository mounted +at `/workspace` and `/workspace` as the working directory: ```shell +# ReforceXY python -m unittest discover -s ReforceXY/tests -v + +# QuickAdapter +PYTHONPATH=/workspace/quickadapter/user_data/strategies \ + python -m unittest discover -s quickadapter/tests -v ``` +CI runs type checks and runtime regressions in one QA matrix entry per strategy. +The shared runtime step sets each strategy's `PYTHONPATH`. QuickAdapter needs +this for direct `unittest` discovery because its model imports `LabelTransformer` +and `Utils` by bare names; ReforceXY resolves its imports without it, so the +setting is optional there. The reward-space analysis suite runs separately +with `uv`, without a Freqtrade image. + ### Quality checks Run repository quality checks from the repository root: diff --git a/ReforceXY/.basedpyright/diagnostics.json b/ReforceXY/.basedpyright/diagnostics.json index a305e4b..1bacced 100644 --- a/ReforceXY/.basedpyright/diagnostics.json +++ b/ReforceXY/.basedpyright/diagnostics.json @@ -33,1263 +33,1093 @@ }, { "endCharacter": 5, - "endLine": 2095, + "endLine": 2136, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "No overloads for \"cut\" match the provided arguments", "rule": "reportCallIssue", "severity": "error", "startCharacter": 17, - "startLine": 2090 + "startLine": 2131 }, { "endCharacter": 21, - "endLine": 2092, + "endLine": 2133, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Argument of type \"_Array1D[Any]\" cannot be assigned to parameter \"bins\" of type \"int | Sequence[float] | Index[int] | Index[float] | IntervalIndex[Interval[Any]] | Series[Any]\" in function \"cut\"\n  Type \"_Array1D[Any]\" is not assignable to type \"int | Sequence[float] | Index[int] | Index[float] | IntervalIndex[Interval[Any]] | Series[Any]\"\n    \"ndarray[tuple[int], dtype[Any]]\" is not assignable to \"int\"\n    \"ndarray[tuple[int], dtype[Any]]\" is not assignable to \"Sequence[float]\"\n    \"ndarray[tuple[int], dtype[Any]]\" is not assignable to \"Index[int]\"\n    \"ndarray[tuple[int], dtype[Any]]\" is not assignable to \"Index[float]\"\n    \"ndarray[tuple[int], dtype[Any]]\" is not assignable to \"IntervalIndex[Interval[Any]]\"\n    \"ndarray[tuple[int], dtype[Any]]\" is not assignable to \"Series[Any]\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 13, - "startLine": 2092 + "startLine": 2133 }, { "endCharacter": 5, - "endLine": 2117, + "endLine": 2158, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Type \"dict[bytes, bytes] | dict[str, RewardParamValue]\" is not assignable to declared type \"RewardParams\"\n  Type \"dict[bytes, bytes] | dict[str, RewardParamValue]\" is not assignable to type \"RewardParams\"\n    \"dict[bytes, bytes]\" is not assignable to \"dict[str, RewardParamValue]\"\n      Type parameter \"_KT@dict\" is invariant, but \"bytes\" is not the same as \"str\"\n      Type parameter \"_VT@dict\" is invariant, but \"bytes\" is not the same as \"RewardParamValue\"", "rule": "reportAssignmentType", "severity": "error", "startCharacter": 34, - "startLine": 2113 + "startLine": 2154 }, { "endCharacter": 43, - "endLine": 2114, + "endLine": 2155, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "No overloads for \"__init__\" match the provided arguments", "rule": "reportCallIssue", "severity": "error", "startCharacter": 8, - "startLine": 2114 + "startLine": 2155 }, { "endCharacter": 42, - "endLine": 2114, + "endLine": 2155, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"iterable\" of type \"Iterable[list[bytes]]\" in function \"__init__\"\n  Type \"Any | None\" is not assignable to type \"Iterable[list[bytes]]\"\n    \"None\" is incompatible with protocol \"Iterable[list[bytes]]\"\n      \"__iter__\" is not present", "rule": "reportArgumentType", "severity": "error", "startCharacter": 13, - "startLine": 2114 + "startLine": 2155 }, { "endCharacter": 60, - "endLine": 2374, + "endLine": 2415, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Cannot access attribute \"importances_mean\" for class \"dict[Unknown, Bunch]\"\n  Attribute \"importances_mean\" is unknown", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 44, - "startLine": 2374 + "startLine": 2415 }, { "endCharacter": 58, - "endLine": 2375, + "endLine": 2416, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Cannot access attribute \"importances_std\" for class \"dict[Unknown, Bunch]\"\n  Attribute \"importances_std\" is unknown", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 43, - "startLine": 2375 + "startLine": 2416 }, { "endCharacter": 88, - "endLine": 2394, + "endLine": 2435, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Cannot access attribute \"columns\" for class \"NDArray[Unknown]\"\n  Attribute \"columns\" is unknown", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 81, - "startLine": 2394 + "startLine": 2435 }, { "endCharacter": 88, - "endLine": 2394, + "endLine": 2435, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Cannot access attribute \"columns\" for class \"list[Unknown]\"\n  Attribute \"columns\" is unknown", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 81, - "startLine": 2394 + "startLine": 2435 }, { "endCharacter": 17, - "endLine": 2403, + "endLine": 2444, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Object of type \"None\" cannot be called", "rule": "reportOptionalCall", "severity": "error", "startCharacter": 28, - "startLine": 2397 - }, - { - "endCharacter": 40, - "endLine": 2632, - "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", - "message": "Argument of type \"Any | object | NAType | Unknown\" cannot be assigned to parameter \"arg1\" of type \"SupportsRichComparisonT@min\" in function \"min\"\n  Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n    Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n      Type \"object\" is not assignable to type \"SupportsRichComparison\"\n        \"object\" is incompatible with protocol \"SupportsDunderLT[Any]\"\n          \"__lt__\" is not present\n        \"object\" is incompatible with protocol \"SupportsDunderGT[Any]\"\n          \"__gt__\" is not present", - "rule": "reportArgumentType", - "severity": "error", - "startCharacter": 22, - "startLine": 2632 - }, - { - "endCharacter": 38, - "endLine": 2632, - "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", - "message": "Cannot access attribute \"min\" for class \"ExtensionArray\"\n  Attribute \"min\" is unknown", - "rule": "reportAttributeAccessIssue", - "severity": "error", - "startCharacter": 35, - "startLine": 2632 - }, - { - "endCharacter": 59, - "endLine": 2632, - "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", - "message": "Argument of type \"Any | object | NAType | Unknown\" cannot be assigned to parameter \"arg2\" of type \"SupportsRichComparisonT@min\" in function \"min\"\n  Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n    Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n      Type \"object\" is not assignable to type \"SupportsRichComparison\"\n        \"object\" is incompatible with protocol \"SupportsDunderLT[Any]\"\n          \"__lt__\" is not present\n        \"object\" is incompatible with protocol \"SupportsDunderGT[Any]\"\n          \"__gt__\" is not present", - "rule": "reportArgumentType", - "severity": "error", - "startCharacter": 42, - "startLine": 2632 - }, - { - "endCharacter": 57, - "endLine": 2632, - "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", - "message": "Cannot access attribute \"min\" for class \"ExtensionArray\"\n  Attribute \"min\" is unknown", - "rule": "reportAttributeAccessIssue", - "severity": "error", - "startCharacter": 54, - "startLine": 2632 - }, - { - "endCharacter": 40, - "endLine": 2633, - "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", - "message": "Argument of type \"Any | object | NAType | Unknown\" cannot be assigned to parameter \"arg1\" of type \"SupportsRichComparisonT@max\" in function \"max\"\n  Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n    Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n      Type \"object\" is not assignable to type \"SupportsRichComparison\"\n        \"object\" is incompatible with protocol \"SupportsDunderLT[Any]\"\n          \"__lt__\" is not present\n        \"object\" is incompatible with protocol \"SupportsDunderGT[Any]\"\n          \"__gt__\" is not present", - "rule": "reportArgumentType", - "severity": "error", - "startCharacter": 22, - "startLine": 2633 - }, - { - "endCharacter": 38, - "endLine": 2633, - "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", - "message": "Cannot access attribute \"max\" for class \"ExtensionArray\"\n  Attribute \"max\" is unknown", - "rule": "reportAttributeAccessIssue", - "severity": "error", - "startCharacter": 35, - "startLine": 2633 - }, - { - "endCharacter": 59, - "endLine": 2633, - "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", - "message": "Argument of type \"Any | object | NAType | Unknown\" cannot be assigned to parameter \"arg2\" of type \"SupportsRichComparisonT@max\" in function \"max\"\n  Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n    Type \"Any | object | NAType | Unknown\" is not assignable to type \"SupportsRichComparison\"\n      Type \"object\" is not assignable to type \"SupportsRichComparison\"\n        \"object\" is incompatible with protocol \"SupportsDunderLT[Any]\"\n          \"__lt__\" is not present\n        \"object\" is incompatible with protocol \"SupportsDunderGT[Any]\"\n          \"__gt__\" is not present", - "rule": "reportArgumentType", - "severity": "error", - "startCharacter": 42, - "startLine": 2633 - }, - { - "endCharacter": 57, - "endLine": 2633, - "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", - "message": "Cannot access attribute \"max\" for class \"ExtensionArray\"\n  Attribute \"max\" is unknown", - "rule": "reportAttributeAccessIssue", - "severity": "error", - "startCharacter": 54, - "startLine": 2633 - }, - { - "endCharacter": 76, - "endLine": 2649, - "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", - "message": "No overloads for \"histogram\" match the provided arguments", - "rule": "reportCallIssue", - "severity": "error", - "startCharacter": 24, - "startLine": 2649 - }, - { - "endCharacter": 49, - "endLine": 2649, - "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", - "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"a\" of type \"_ArrayLikeComplex_co\" in function \"histogram\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"_ArrayLikeComplex_co\"\n    Type \"ExtensionArray\" is not assignable to type \"_ArrayLikeComplex_co\"\n      \"ExtensionArray\" is incompatible with protocol \"_SupportsArray[dtype[numpy.bool[builtins.bool] | number[Any, Any]]]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"_NestedSequence[_SupportsArray[dtype[numpy.bool[builtins.bool] | number[Any, Any]]]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n ...", - "rule": "reportArgumentType", - "severity": "error", - "startCharacter": 37, - "startLine": 2649 - }, - { - "endCharacter": 74, - "endLine": 2650, - "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", - "message": "No overloads for \"histogram\" match the provided arguments", - "rule": "reportCallIssue", - "severity": "error", - "startCharacter": 23, - "startLine": 2650 - }, - { - "endCharacter": 47, - "endLine": 2650, - "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", - "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"a\" of type \"_ArrayLikeComplex_co\" in function \"histogram\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"_ArrayLikeComplex_co\"\n    Type \"ExtensionArray\" is not assignable to type \"_ArrayLikeComplex_co\"\n      \"ExtensionArray\" is incompatible with protocol \"_SupportsArray[dtype[numpy.bool[builtins.bool] | number[Any, Any]]]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"_NestedSequence[_SupportsArray[dtype[numpy.bool[builtins.bool] | number[Any, Any]]]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n ...", - "rule": "reportArgumentType", - "severity": "error", - "startCharacter": 36, - "startLine": 2650 - }, - { - "endCharacter": 51, - "endLine": 2678, - "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", - "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"u_values\" of type \"ToFloatND\" in function \"wasserstein_distance\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"ToFloatND\"\n    Type \"ExtensionArray\" is not assignable to type \"ToFloatND\"\n      \"ExtensionArray\" is incompatible with protocol \"_CanArrayND[floating_co]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"SequenceND[py_float | _CanArray[floating_co]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n ...", - "rule": "reportArgumentType", - "severity": "error", - "startCharacter": 39, - "startLine": 2678 - }, - { - "endCharacter": 64, - "endLine": 2678, - "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", - "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"v_values\" of type \"ToFloatND\" in function \"wasserstein_distance\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"ToFloatND\"\n    Type \"ExtensionArray\" is not assignable to type \"ToFloatND\"\n      \"ExtensionArray\" is incompatible with protocol \"_CanArrayND[floating_co]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"SequenceND[py_float | _CanArray[floating_co]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n ...", - "rule": "reportArgumentType", - "severity": "error", - "startCharacter": 53, - "startLine": 2678 - }, - { - "endCharacter": 68, - "endLine": 2681, - "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", - "message": "No overloads for \"ks_2samp\" match the provided arguments", - "rule": "reportCallIssue", - "severity": "error", - "startCharacter": 27, - "startLine": 2681 - }, - { - "endCharacter": 54, - "endLine": 2681, - "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", - "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"data1\" of type \"ToFloatND\" in function \"ks_2samp\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"ToFloatND\"\n    Type \"ExtensionArray\" is not assignable to type \"ToFloatND\"\n      \"ExtensionArray\" is incompatible with protocol \"_CanArrayND[floating_co]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"SequenceND[py_float | _CanArray[floating_co]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n ...", - "rule": "reportArgumentType", - "severity": "error", - "startCharacter": 42, - "startLine": 2681 - }, - { - "endCharacter": 67, - "endLine": 2681, - "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", - "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"data2\" of type \"ToFloatND\" in function \"ks_2samp\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"ToFloatND\"\n    Type \"ExtensionArray\" is not assignable to type \"ToFloatND\"\n      \"ExtensionArray\" is incompatible with protocol \"_CanArrayND[floating_co]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"SequenceND[py_float | _CanArray[floating_co]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n ...", - "rule": "reportArgumentType", - "severity": "error", - "startCharacter": 56, - "startLine": 2681 + "startLine": 2438 }, { "endCharacter": 29, - "endLine": 2981, + "endLine": 3043, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "No overloads for \"ptp\" match the provided arguments", "rule": "reportCallIssue", "severity": "error", "startCharacter": 11, - "startLine": 2981 + "startLine": 3043 }, { "endCharacter": 28, - "endLine": 2981, + "endLine": 3043, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Argument of type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" cannot be assigned to parameter \"a\" of type \"_ArrayLikeNumeric_co\" in function \"ptp\"\n  Type \"np_1darray[Any] | ExtensionArray | Categorical[object]\" is not assignable to type \"_ArrayLikeNumeric_co\"\n    Type \"ExtensionArray\" is not assignable to type \"_ArrayLikeNumeric_co\"\n      \"ExtensionArray\" is incompatible with protocol \"_SupportsArray[dtype[number[Any, Any] | numpy.bool[builtins.bool] | object_ | timedelta64[Any]]]\"\n        \"__array__\" is not present\n      \"ExtensionArray\" is incompatible with protocol \"_NestedSequence[_SupportsArray[dtype[number[Any, Any] | numpy.bool[builtins.bool] | object_ | timedelta64[Any]]]]\"\n        \"__reversed__\" is not present\n        \"count\" is not present\n        \"index\" is not present\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 18, - "startLine": 2981 + "startLine": 3043 }, { "endCharacter": 65, - "endLine": 2990, + "endLine": 3052, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Cannot access attribute \"mean\" for class \"Categorical[object]\"\n  Attribute \"mean\" is unknown", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 61, - "startLine": 2990 + "startLine": 3052 }, { "endCharacter": 65, - "endLine": 2990, + "endLine": 3052, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Cannot access attribute \"mean\" for class \"ExtensionArray\"\n  Attribute \"mean\" is unknown", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 61, - "startLine": 2990 + "startLine": 3052 }, { "endCharacter": 5, - "endLine": 3886, + "endLine": 3952, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Type \"dict[bytes, bytes] | dict[str, RewardParamValue]\" is not assignable to declared type \"RewardParams\"\n  Type \"dict[bytes, bytes] | dict[str, RewardParamValue]\" is not assignable to type \"RewardParams\"\n    \"dict[bytes, bytes]\" is not assignable to \"dict[str, RewardParamValue]\"\n      Type parameter \"_KT@dict\" is invariant, but \"bytes\" is not the same as \"str\"\n      Type parameter \"_VT@dict\" is invariant, but \"bytes\" is not the same as \"RewardParamValue\"", "rule": "reportAssignmentType", "severity": "error", "startCharacter": 34, - "startLine": 3882 + "startLine": 3948 }, { "endCharacter": 43, - "endLine": 3883, + "endLine": 3949, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "No overloads for \"__init__\" match the provided arguments", "rule": "reportCallIssue", "severity": "error", "startCharacter": 8, - "startLine": 3883 + "startLine": 3949 }, { "endCharacter": 42, - "endLine": 3883, + "endLine": 3949, "file": "ReforceXY/reward_space_analysis/reward_space_analysis.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"iterable\" of type \"Iterable[list[bytes]]\" in function \"__init__\"\n  Type \"Any | None\" is not assignable to type \"Iterable[list[bytes]]\"\n    \"None\" is incompatible with protocol \"Iterable[list[bytes]]\"\n      \"__iter__\" is not present", "rule": "reportArgumentType", "severity": "error", "startCharacter": 13, - "startLine": 3883 + "startLine": 3949 }, { "endCharacter": 22, - "endLine": 1010, + "endLine": 1011, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Cannot assign to attribute \"train_env\" for class \"ReforceXY*\"\n  Expression of type \"VecEnv\" cannot be assigned to attribute \"train_env\" of class \"ReforceXY\"\n    Type \"VecEnv\" is not assignable to type \"VecMonitor | SubprocVecEnv | Env[Unknown, Unknown]\"\n      \"VecEnv\" is not assignable to \"VecMonitor\"\n      \"VecEnv\" is not assignable to \"SubprocVecEnv\"\n      \"VecEnv\" is not assignable to \"Env[Unknown, Unknown]\"", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 13, - "startLine": 1010 + "startLine": 1011 }, { "endCharacter": 37, - "endLine": 1010, + "endLine": 1011, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Cannot assign to attribute \"eval_env\" for class \"ReforceXY*\"\n  Expression of type \"VecEnv | None\" cannot be assigned to attribute \"eval_env\" of class \"ReforceXY\"\n    Type \"VecEnv | None\" is not assignable to type \"VecMonitor | SubprocVecEnv | Env[Unknown, Unknown]\"\n      Type \"VecEnv\" is not assignable to type \"VecMonitor | SubprocVecEnv | Env[Unknown, Unknown]\"\n        \"VecEnv\" is not assignable to \"VecMonitor\"\n        \"VecEnv\" is not assignable to \"SubprocVecEnv\"\n        \"VecEnv\" is not assignable to \"Env[Unknown, Unknown]\"", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 29, - "startLine": 1010 + "startLine": 1011 }, { "endCharacter": 24, - "endLine": 1269, + "endLine": 1270, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"VecEnv\" cannot be assigned to parameter \"eval_env\" of type \"BaseEnvironment\" in function \"__init__\"\n  \"VecEnv\" is not assignable to \"BaseEnvironment\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 16, - "startLine": 1269 + "startLine": 1270 }, { "endCharacter": 38, - "endLine": 1477, + "endLine": 1498, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"obj\" of type \"Sized\" in function \"len\"\n  Type \"Any | None\" is not assignable to type \"Sized\"\n    \"None\" is incompatible with protocol \"Sized\"\n      \"__len__\" is not present", "rule": "reportArgumentType", "severity": "error", "startCharacter": 30, - "startLine": 1477 + "startLine": 1498 }, { "endCharacter": 36, - "endLine": 1481, + "endLine": 1502, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"obj\" of type \"Sized\" in function \"len\"\n  Type \"Any | None\" is not assignable to type \"Sized\"\n    \"None\" is incompatible with protocol \"Sized\"\n      \"__len__\" is not present", "rule": "reportArgumentType", "severity": "error", "startCharacter": 29, - "startLine": 1481 + "startLine": 1502 }, { "endCharacter": 80, - "endLine": 1484, + "endLine": 1505, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"timeframe\" of type \"str\" in function \"steps_to_days\"\n  Type \"Any | None\" is not assignable to type \"str\"\n    \"None\" is not assignable to \"str\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 52, - "startLine": 1484 + "startLine": 1505 }, { "endCharacter": 78, - "endLine": 1485, + "endLine": 1506, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"timeframe\" of type \"str\" in function \"steps_to_days\"\n  Type \"Any | None\" is not assignable to type \"str\"\n    \"None\" is not assignable to \"str\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 50, - "startLine": 1485 + "startLine": 1506 }, { "endCharacter": 80, - "endLine": 1486, + "endLine": 1507, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"timeframe\" of type \"str\" in function \"steps_to_days\"\n  Type \"Any | None\" is not assignable to type \"str\"\n    \"None\" is not assignable to \"str\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 52, - "startLine": 1486 + "startLine": 1507 }, { "endCharacter": 56, - "endLine": 1571, + "endLine": 1592, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"VecMonitor | SubprocVecEnv | Env[Unknown, Unknown]\" cannot be assigned to parameter \"eval_env\" of type \"VecEnv | None\" in function \"get_callbacks\"\n  Type \"VecMonitor | SubprocVecEnv | Env[Unknown, Unknown]\" is not assignable to type \"VecEnv | None\"\n    Type \"Env[Unknown, Unknown]\" is not assignable to type \"VecEnv | None\"\n      \"Env[Unknown, Unknown]\" is not assignable to \"VecEnv\"\n      \"Env[Unknown, Unknown]\" is not assignable to \"None\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 43, - "startLine": 1571 + "startLine": 1592 }, { "endCharacter": 84, - "endLine": 1591, + "endLine": 1612, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | list[float]\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | list[float]\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"list[float]\" is not assignable to type \"ConvertibleToFloat\"\n      \"list[float]\" is not assignable to \"str\"\n      \"list[float]\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"list[float]\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"list[float]\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 67, - "startLine": 1591 + "startLine": 1612 }, { "endCharacter": 84, - "endLine": 1591, + "endLine": 1612, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | list[float]\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | list[float]\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"list[float]\" is not assignable to type \"ConvertibleToFloat\"\n      \"list[float]\" is not assignable to \"str\"\n      \"list[float]\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"list[float]\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"list[float]\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 67, - "startLine": 1591 + "startLine": 1612 }, { "endCharacter": 44, - "endLine": 2665, + "endLine": 2686, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "\"best_trial_params\" is possibly unbound", "rule": "reportPossiblyUnboundVariable", "severity": "error", "startCharacter": 27, - "startLine": 2665 + "startLine": 2686 }, { "endCharacter": 12, - "endLine": 2685, + "endLine": 2706, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Parameter declaration \"seed\" is obscured by a declaration of the same name", "rule": "reportRedeclaration", "severity": "error", "startCharacter": 8, - "startLine": 2685 + "startLine": 2706 }, { "endCharacter": 16, - "endLine": 2686, + "endLine": 2707, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Parameter declaration \"env_info\" is obscured by a declaration of the same name", "rule": "reportRedeclaration", "severity": "error", "startCharacter": 8, - "startLine": 2686 + "startLine": 2707 }, { "endCharacter": 51, - "endLine": 2729, + "endLine": 2750, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"list[() -> BaseEnvironment]\" cannot be assigned to parameter \"env_fns\" of type \"list[() -> Env[Unknown, Unknown]]\" in function \"__init__\"\n  \"list[() -> BaseEnvironment]\" is not assignable to \"list[() -> Env[Unknown, Unknown]]\"\n    Type parameter \"_T@list\" is invariant, but \"() -> BaseEnvironment\" is not the same as \"() -> Env[Unknown, Unknown]\"\n    Consider switching from \"list\" to \"Sequence\" which is covariant", "rule": "reportArgumentType", "severity": "error", "startCharacter": 42, - "startLine": 2729 + "startLine": 2750 }, { "endCharacter": 53, - "endLine": 2735, + "endLine": 2756, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"list[() -> BaseEnvironment]\" cannot be assigned to parameter \"env_fns\" of type \"list[() -> Env[Unknown, Unknown]]\" in function \"__init__\"\n  \"list[() -> BaseEnvironment]\" is not assignable to \"list[() -> Env[Unknown, Unknown]]\"\n    Type parameter \"_T@list\" is invariant, but \"() -> BaseEnvironment\" is not the same as \"() -> Env[Unknown, Unknown]\"\n    Consider switching from \"list\" to \"Sequence\" which is covariant", "rule": "reportArgumentType", "severity": "error", "startCharacter": 45, - "startLine": 2735 + "startLine": 2756 }, { "endCharacter": 22, - "endLine": 2798, + "endLine": 2819, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Operator \"*\" not supported for \"None\"", "rule": "reportOptionalOperand", "severity": "error", "startCharacter": 15, - "startLine": 2798 + "startLine": 2819 }, { "endCharacter": 96, - "endLine": 2800, + "endLine": 2821, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Operator \"*\" not supported for \"None\"", "rule": "reportOptionalOperand", "severity": "error", "startCharacter": 89, - "startLine": 2800 + "startLine": 2821 }, { "endCharacter": 23, - "endLine": 2803, + "endLine": 2824, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Operator \"*\" not supported for \"None\"", "rule": "reportOptionalOperand", "severity": "error", "startCharacter": 16, - "startLine": 2803 + "startLine": 2824 }, { "endCharacter": 98, - "endLine": 2805, + "endLine": 2826, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Operator \"*\" not supported for \"None\"", "rule": "reportOptionalOperand", "severity": "error", "startCharacter": 91, - "startLine": 2805 + "startLine": 2826 }, { "endCharacter": 44, - "endLine": 2817, + "endLine": 2838, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Operator \"*\" not supported for types \"Any | None\" and \"Any | int | None\"\n  Operator \"*\" not supported for types \"None\" and \"None\"\n  Operator \"*\" not supported for types \"None\" and \"int\"", "rule": "reportOperatorIssue", "severity": "error", "startCharacter": 15, - "startLine": 2817 + "startLine": 2838 }, { "endCharacter": 133, - "endLine": 2819, + "endLine": 2840, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Operator \"*\" not supported for types \"Any | None\" and \"Any | int | None\"\n  Operator \"*\" not supported for types \"None\" and \"None\"\n  Operator \"*\" not supported for types \"None\" and \"int\"", "rule": "reportOperatorIssue", "severity": "error", "startCharacter": 106, - "startLine": 2819 + "startLine": 2840 }, { "endCharacter": 30, - "endLine": 2822, + "endLine": 2843, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Operator \">\" not supported for \"None\"", "rule": "reportOptionalOperand", "severity": "error", "startCharacter": 15, - "startLine": 2822 + "startLine": 2843 }, { "endCharacter": 54, - "endLine": 2887, + "endLine": 2908, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "\"is_pruned\" is not a known attribute of \"None\"", "rule": "reportOptionalMemberAccess", "severity": "error", "startCharacter": 45, - "startLine": 2887 + "startLine": 2908 }, { "endCharacter": 67, - "endLine": 2890, + "endLine": 2911, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "\"use_masking\" is not a known attribute of \"None\"", "rule": "reportOptionalMemberAccess", "severity": "error", "startCharacter": 56, - "startLine": 2890 + "startLine": 2911 }, { "endCharacter": 60, - "endLine": 2898, + "endLine": 2919, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "\"update_best_reward\" is not a known attribute of \"None\"", "rule": "reportOptionalMemberAccess", "severity": "error", "startCharacter": 42, - "startLine": 2898 + "startLine": 2919 }, { "endCharacter": 84, - "endLine": 2898, + "endLine": 2919, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | list[float]\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | list[float]\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"list[float]\" is not assignable to type \"ConvertibleToFloat\"\n      \"list[float]\" is not assignable to \"str\"\n      \"list[float]\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"list[float]\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"list[float]\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 67, - "startLine": 2898 + "startLine": 2919 }, { "endCharacter": 84, - "endLine": 2898, + "endLine": 2919, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | list[float]\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | list[float]\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"list[float]\" is not assignable to type \"ConvertibleToFloat\"\n      \"list[float]\" is not assignable to \"str\"\n      \"list[float]\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"list[float]\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"list[float]\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 67, - "startLine": 2898 + "startLine": 2919 }, { "endCharacter": 46, - "endLine": 2952, + "endLine": 2973, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "\"is_pruned\" is not a known attribute of \"None\"", "rule": "reportOptionalMemberAccess", "severity": "error", "startCharacter": 37, - "startLine": 2952 + "startLine": 2973 }, { "endCharacter": 57, - "endLine": 2955, + "endLine": 2976, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "\"best_mean_reward\" is not a known attribute of \"None\"", "rule": "reportOptionalMemberAccess", "severity": "error", "startCharacter": 41, - "startLine": 2955 + "startLine": 2976 }, { "endCharacter": 37, - "endLine": 2965, + "endLine": 2986, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Cannot assign to attribute \"train_env\" for class \"ReforceXY*\"\n  Type \"None\" is not assignable to type \"VecMonitor | SubprocVecEnv | Env[Unknown, Unknown]\"\n    \"None\" is not assignable to \"VecMonitor\"\n    \"None\" is not assignable to \"SubprocVecEnv\"\n    \"None\" is not assignable to \"Env[Unknown, Unknown]\"", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 33, - "startLine": 2965 + "startLine": 2986 }, { "endCharacter": 36, - "endLine": 2970, + "endLine": 2991, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Cannot assign to attribute \"eval_env\" for class \"ReforceXY*\"\n  Type \"None\" is not assignable to type \"VecMonitor | SubprocVecEnv | Env[Unknown, Unknown]\"\n    \"None\" is not assignable to \"VecMonitor\"\n    \"None\" is not assignable to \"SubprocVecEnv\"\n    \"None\" is not assignable to \"Env[Unknown, Unknown]\"", "rule": "reportAttributeAccessIssue", "severity": "error", "startCharacter": 32, - "startLine": 2970 + "startLine": 2991 }, { "endCharacter": 7, - "endLine": 3015, + "endLine": 3036, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Declaration \"MyRLEnv\" is obscured by a declaration of the same name", "rule": "reportRedeclaration", "severity": "error", "startCharacter": 0, - "startLine": 3015 + "startLine": 3036 }, { "endCharacter": 36, - "endLine": 3027, + "endLine": 3048, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Declaration \"_last_closed_trade_tick\" is obscured by a declaration of the same name", "rule": "reportRedeclaration", "severity": "error", "startCharacter": 13, - "startLine": 3027 + "startLine": 3048 }, { "endCharacter": 13, - "endLine": 3619, + "endLine": 3640, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Method \"reset\" overrides class \"BaseEnvironment\" in an incompatible manner\n  Return type mismatch: base method returns type \"tuple[DataFrame, dict[Unknown, Unknown]]\", override returns type \"tuple[NDArray[float32], dict[str, Any]]\"\n    \"tuple[NDArray[float32], dict[str, Any]]\" is not assignable to \"tuple[DataFrame, dict[Unknown, Unknown]]\"\n      Tuple entry 1 is incorrect type\n        \"ndarray[_AnyShape, dtype[float32]]\" is not assignable to \"DataFrame\"", "rule": "reportIncompatibleMethodOverride", "severity": "error", "startCharacter": 8, - "startLine": 3619 + "startLine": 3640 }, { "endCharacter": 26, - "endLine": 3640, + "endLine": 3661, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Type \"tuple[DataFrame, dict[Unknown, Unknown]]\" is not assignable to return type \"tuple[NDArray[float32], dict[str, Any]]\"\n  \"DataFrame\" is not assignable to \"ndarray[_AnyShape, dtype[float32]]\"", "rule": "reportReturnType", "severity": "error", "startCharacter": 15, - "startLine": 3640 + "startLine": 3661 }, { "endCharacter": 55, - "endLine": 3948, + "endLine": 3969, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 44, - "startLine": 3948 + "startLine": 3969 }, { "endCharacter": 55, - "endLine": 3948, + "endLine": 3969, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 44, - "startLine": 3948 + "startLine": 3969 }, { "endCharacter": 59, - "endLine": 3972, + "endLine": 3993, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 48, - "startLine": 3972 + "startLine": 3993 }, { "endCharacter": 59, - "endLine": 3972, + "endLine": 3993, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 48, - "startLine": 3972 + "startLine": 3993 }, { "endCharacter": 24, - "endLine": 4001, + "endLine": 4022, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Method \"_get_observation\" overrides class \"BaseEnvironment\" in an incompatible manner\n  Return type mismatch: base method returns type \"DataFrame\", override returns type \"NDArray[float32]\"\n    \"ndarray[_AnyShape, dtype[float32]]\" is not assignable to \"DataFrame\"", "rule": "reportIncompatibleMethodOverride", "severity": "error", "startCharacter": 8, - "startLine": 4001 + "startLine": 4022 }, { "endCharacter": 12, - "endLine": 4103, + "endLine": 4124, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Method \"step\" overrides class \"Base5ActionRLEnv\" in an incompatible manner\n  Return type mismatch: base method returns type \"tuple[DataFrame, float, bool, Literal[False], dict[str, Unknown]]\", override returns type \"tuple[NDArray[float32], float, bool, bool, dict[str, Any]]\"\n    \"tuple[NDArray[float32], float, bool, bool, dict[str, Any]]\" is not assignable to \"tuple[DataFrame, float, bool, Literal[False], dict[str, Unknown]]\"\n      Tuple entry 1 is incorrect type\n        \"ndarray[_AnyShape, dtype[float32]]\" is not assignable to \"DataFrame\"", "rule": "reportIncompatibleMethodOverride", "severity": "error", "startCharacter": 8, - "startLine": 4103 + "startLine": 4124 }, { "endCharacter": 20, - "endLine": 4290, + "endLine": 4314, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Method \"action_masks\" overrides class \"BaseEnvironment\" in an incompatible manner\n  Return type mismatch: base method returns type \"list[bool]\", override returns type \"NDArray[bool_]\"\n    \"ndarray[_AnyShape, dtype[bool_]]\" is not assignable to \"list[bool]\"", "rule": "reportIncompatibleMethodOverride", "severity": "error", "startCharacter": 8, - "startLine": 4290 + "startLine": 4314 }, { "endCharacter": 40, - "endLine": 4439, + "endLine": 4463, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "\"Figure\" is not exported from module \"matplotlib.pyplot\"", "rule": "reportPrivateImportUsage", "severity": "error", "startCharacter": 34, - "startLine": 4439 + "startLine": 4463 }, { "endCharacter": 54, - "endLine": 4942, + "endLine": 4966, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 45, - "startLine": 4942 + "startLine": 4966 }, { "endCharacter": 54, - "endLine": 4942, + "endLine": 4966, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 45, - "startLine": 4942 + "startLine": 4966 }, { "endCharacter": 50, - "endLine": 4954, + "endLine": 4978, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"object\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"object\" is not assignable to type \"ConvertibleToFloat\"\n    \"object\" is not assignable to \"str\"\n    \"object\" is incompatible with protocol \"Buffer\"\n      \"__buffer__\" is not present\n    \"object\" is incompatible with protocol \"SupportsFloat\"\n      \"__float__\" is not present\n    \"object\" is incompatible with protocol \"SupportsIndex\"\n      \"__index__\" is not present", "rule": "reportArgumentType", "severity": "error", "startCharacter": 37, - "startLine": 4954 + "startLine": 4978 }, { "endCharacter": 72, - "endLine": 4965, + "endLine": 4989, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 70, - "startLine": 4965 + "startLine": 4989 }, { "endCharacter": 72, - "endLine": 4965, + "endLine": 4989, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 70, - "startLine": 4965 + "startLine": 4989 }, { "endCharacter": 73, - "endLine": 4974, + "endLine": 4998, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 71, - "startLine": 4974 + "startLine": 4998 }, { "endCharacter": 73, - "endLine": 4974, + "endLine": 4998, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"float | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"float | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 71, - "startLine": 4974 + "startLine": 4998 }, { "endCharacter": 58, - "endLine": 4983, + "endLine": 5007, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 56, - "startLine": 4983 + "startLine": 5007 }, { "endCharacter": 58, - "endLine": 4983, + "endLine": 5007, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 56, - "startLine": 4983 + "startLine": 5007 }, { "endCharacter": 50, - "endLine": 5214, + "endLine": 5238, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Mapping[Unknown, Unknown]\" cannot be assigned to parameter \"src\" of type \"dict[str, Any]\" in function \"deepmerge\"\n  \"Mapping[Unknown, Unknown]\" is not assignable to \"dict[str, Any]\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 49, - "startLine": 5214 + "startLine": 5238 }, { "endCharacter": 20, - "endLine": 5391, + "endLine": 5415, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 18, - "startLine": 5391 + "startLine": 5415 }, { "endCharacter": 20, - "endLine": 5391, + "endLine": 5415, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 18, - "startLine": 5391 + "startLine": 5415 }, { "endCharacter": 59, - "endLine": 5396, + "endLine": 5420, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 31, - "startLine": 5396 + "startLine": 5420 }, { "endCharacter": 59, - "endLine": 5396, + "endLine": 5420, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 31, - "startLine": 5396 + "startLine": 5420 }, { "endCharacter": 65, - "endLine": 5397, + "endLine": 5421, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 34, - "startLine": 5397 + "startLine": 5421 }, { "endCharacter": 65, - "endLine": 5397, + "endLine": 5421, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 34, - "startLine": 5397 + "startLine": 5421 }, { "endCharacter": 57, - "endLine": 5398, + "endLine": 5422, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 31, - "startLine": 5398 + "startLine": 5422 }, { "endCharacter": 57, - "endLine": 5398, + "endLine": 5422, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 31, - "startLine": 5398 + "startLine": 5422 }, { "endCharacter": 63, - "endLine": 5400, + "endLine": 5424, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 34, - "startLine": 5400 + "startLine": 5424 }, { "endCharacter": 63, - "endLine": 5400, + "endLine": 5424, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 34, - "startLine": 5400 + "startLine": 5424 }, { "endCharacter": 61, - "endLine": 5402, + "endLine": 5426, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 32, - "startLine": 5402 + "startLine": 5426 }, { "endCharacter": 61, - "endLine": 5402, + "endLine": 5426, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 32, - "startLine": 5402 + "startLine": 5426 }, { "endCharacter": 67, - "endLine": 5403, + "endLine": 5427, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 36, - "startLine": 5403 + "startLine": 5427 }, { "endCharacter": 67, - "endLine": 5403, + "endLine": 5427, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 36, - "startLine": 5403 + "startLine": 5427 }, { "endCharacter": 73, - "endLine": 5404, + "endLine": 5428, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 39, - "startLine": 5404 + "startLine": 5428 }, { "endCharacter": 73, - "endLine": 5404, + "endLine": 5428, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 39, - "startLine": 5404 + "startLine": 5428 }, { "endCharacter": 61, - "endLine": 5405, + "endLine": 5429, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 33, - "startLine": 5405 + "startLine": 5429 }, { "endCharacter": 61, - "endLine": 5405, + "endLine": 5429, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 33, - "startLine": 5405 + "startLine": 5429 }, { "endCharacter": 89, - "endLine": 5412, + "endLine": 5436, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 52, - "startLine": 5412 + "startLine": 5436 }, { "endCharacter": 89, - "endLine": 5412, + "endLine": 5436, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 52, - "startLine": 5412 + "startLine": 5436 }, { "endCharacter": 83, - "endLine": 5413, + "endLine": 5437, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 49, - "startLine": 5413 + "startLine": 5437 }, { "endCharacter": 83, - "endLine": 5413, + "endLine": 5437, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 49, - "startLine": 5413 + "startLine": 5437 }, { "endCharacter": 57, - "endLine": 5438, + "endLine": 5462, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 31, - "startLine": 5438 + "startLine": 5462 }, { "endCharacter": 57, - "endLine": 5438, + "endLine": 5462, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 31, - "startLine": 5438 + "startLine": 5462 }, { "endCharacter": 65, - "endLine": 5439, + "endLine": 5463, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 34, - "startLine": 5439 + "startLine": 5463 }, { "endCharacter": 65, - "endLine": 5439, + "endLine": 5463, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 34, - "startLine": 5439 + "startLine": 5463 }, { "endCharacter": 67, - "endLine": 5441, + "endLine": 5465, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 35, - "startLine": 5441 + "startLine": 5465 }, { "endCharacter": 67, - "endLine": 5441, + "endLine": 5465, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 35, - "startLine": 5441 + "startLine": 5465 }, { "endCharacter": 87, - "endLine": 5444, + "endLine": 5468, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 46, - "startLine": 5444 + "startLine": 5468 }, { "endCharacter": 87, - "endLine": 5444, + "endLine": 5468, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 46, - "startLine": 5444 + "startLine": 5468 }, { "endCharacter": 93, - "endLine": 5445, + "endLine": 5469, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 49, - "startLine": 5445 + "startLine": 5469 }, { "endCharacter": 93, - "endLine": 5445, + "endLine": 5469, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 49, - "startLine": 5445 + "startLine": 5469 }, { "endCharacter": 89, - "endLine": 5446, + "endLine": 5470, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 47, - "startLine": 5446 + "startLine": 5470 }, { "endCharacter": 89, - "endLine": 5446, + "endLine": 5470, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToFloat\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToFloat\"\n    Type \"None\" is not assignable to type \"ConvertibleToFloat\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsFloat\"\n        \"__float__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 47, - "startLine": 5446 + "startLine": 5470 }, { "endCharacter": 89, - "endLine": 5447, + "endLine": 5471, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 46, - "startLine": 5447 + "startLine": 5471 }, { "endCharacter": 89, - "endLine": 5447, + "endLine": 5471, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 46, - "startLine": 5447 + "startLine": 5471 }, { "endCharacter": 75, - "endLine": 5448, + "endLine": 5472, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"", "rule": "reportArgumentType", "severity": "error", "startCharacter": 39, - "startLine": 5448 + "startLine": 5472 }, { "endCharacter": 75, - "endLine": 5448, + "endLine": 5472, "file": "ReforceXY/user_data/freqaimodels/ReforceXY.py", "message": "Argument of type \"Any | None\" cannot be assigned to parameter \"x\" of type \"ConvertibleToInt\" in function \"__new__\"\n  Type \"Any | None\" is not assignable to type \"ConvertibleToInt\"\n    Type \"None\" is not assignable to type \"ConvertibleToInt\"\n      \"None\" is not assignable to \"str\"\n      \"None\" is incompatible with protocol \"Buffer\"\n        \"__buffer__\" is not present\n      \"None\" is incompatible with protocol \"SupportsInt\"\n        \"__int__\" is not present\n      \"None\" is incompatible with protocol \"SupportsIndex\"\n ...", "rule": "reportArgumentType", "severity": "error", "startCharacter": 39, - "startLine": 5448 + "startLine": 5472 }, { "endCharacter": 33, @@ -1313,43 +1143,43 @@ }, { "endCharacter": 17, - "endLine": 62, + "endLine": 63, "file": "ReforceXY/user_data/strategies/RLAgentStrategy.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": 62 + "startLine": 63 }, { "endCharacter": 19, - "endLine": 66, + "endLine": 67, "file": "ReforceXY/user_data/strategies/RLAgentStrategy.py", "message": "\"protections\" overrides symbol of same name in class \"IStrategy\"\n  \"property\" is not assignable to \"list[Unknown]\"", "rule": "reportIncompatibleVariableOverride", "severity": "error", "startCharacter": 8, - "startLine": 66 + "startLine": 67 }, { "endCharacter": 72, - "endLine": 88, + "endLine": 89, "file": "ReforceXY/user_data/strategies/RLAgentStrategy.py", "message": "No overloads for \"__call__\" match the provided arguments", "rule": "reportCallIssue", "severity": "error", "startCharacter": 42, - "startLine": 88 + "startLine": 89 }, { "endCharacter": 71, - "endLine": 88, + "endLine": 89, "file": "ReforceXY/user_data/strategies/RLAgentStrategy.py", "message": "Argument of type \"Series[Any] | None\" cannot be assigned to parameter \"x1\" of type \"_SupportsArrayUFunc\" in function \"__call__\"\n  Type \"Series[Any] | None\" is not assignable to type \"_SupportsArrayUFunc\"\n    \"None\" is incompatible with protocol \"_SupportsArrayUFunc\"\n      \"__array_ufunc__\" is not present", "rule": "reportArgumentType", "severity": "error", "startCharacter": 49, - "startLine": 88 + "startLine": 89 } ], "filesAnalyzed": 3, diff --git a/ReforceXY/reward_space_analysis/README.md b/ReforceXY/reward_space_analysis/README.md index 28ccd42..5000b4e 100644 --- a/ReforceXY/reward_space_analysis/README.md +++ b/ReforceXY/reward_space_analysis/README.md @@ -57,7 +57,7 @@ Full test documentation: [tests/README.md](./tests/README.md). - [Transform Functions](#transform-functions) - [Skipping Feature Analysis](#skipping-feature-analysis) - [Reproducibility](#reproducibility) - - [Overrides vs --params](#overrides-vs--params) + - [Overrides vs --params](#overrides-vs---params) - [Examples](#examples) - [Outputs](#outputs) - [Main Report (`statistical_analysis.md`)](#main-report-statistical_analysismd) @@ -165,10 +165,10 @@ Generates shift metrics for comparison (see Outputs section). - **`--real_episodes`** (path, optional) – Episodes pickle for real vs synthetic distribution shift metrics. (Simulation-only; triggers additional outputs when provided). -- **`--unrealized_pnl`** (flag, default: false) – Transform the retained - in-position synthetic price/PnL trajectory using fee-aware unrealized PnL. - This affects extrema and all enabled base, PBRS, and additive reward terms - that depend on PnL. (Simulation-only.) +- **`--unrealized_pnl`** (flag, default: false) – Track a sampled market + price separately from the retained price within each trade; map its fee-aware + PnL through duration-based tanh scaling before retaining the mark. Retained + marks affect exit-efficiency extrema and PnL-dependent rewards. (Simulation-only.) ### Hybrid Simulation Scalars @@ -178,8 +178,10 @@ be overridden via `--params`. - **`--profit_aim`** (float, default: 0.03) – Profit target threshold (e.g. 0.03=3%). - **`--risk_reward_ratio`** (float, default: 2.0) – Risk-reward multiplier. -- **`--action_masking`** (bool, default: true) – Simulate environment action - masking. Invalid actions receive penalties only if masking disabled. +- **`--action_masking`** (bool, default: true) – With masking enabled, sample + only valid actions. When disabled, sample an invalid action with 10% probability + and apply the configured invalid-action penalty. Invalid actions leave the held + position unchanged, except for an independent terminal liquidation. ### Reward & Shaping @@ -219,6 +221,9 @@ does not expose this option. In that mode, bootstrap percentile intervals retain finite ordered bounds, including exact zero-width intervals for constants; the interval need not contain the original sample mean. +Bootstrap counts must be positive. The PnL rank-biserial effect is positive when +the first named group (`pnl+`) has higher rewards than the second (`pnl-`). + ### Overrides - **`--out_dir`** (path, default: reward_space_outputs) – Output directory @@ -257,15 +262,16 @@ The exit factor is computed as: **Formula:** Let `pnl_target = profit_aim · risk_reward_ratio` and -`pnl_ratio = pnl / pnl_target`. On the loss branch, -`loss_threshold = pnl_target / risk_reward_ratio` and -`loss_ratio = |pnl| / loss_threshold = |pnl_ratio| · risk_reward_ratio`. +`pnl_ratio = pnl / pnl_target` when `pnl_target > 0`. For losses, let +`effective_rr = risk_reward_ratio` if positive, or `2.0` otherwise (the runtime +fallback). Then `loss_threshold = pnl_target / effective_rr` and +`loss_ratio = |pnl| / loss_threshold = |pnl_ratio| · effective_rr`. - If `pnl_target ≤ 0`: `pnl_target_coefficient = 1.0` - If `pnl_ratio > 1.0`: `pnl_target_coefficient = 1.0 + win_reward_factor · tanh(pnl_amplification_sensitivity · (pnl_ratio - 1.0))` - If `pnl < -loss_threshold`: - `pnl_target_coefficient = 1.0 + (win_reward_factor · risk_reward_ratio) · tanh(pnl_amplification_sensitivity · (loss_ratio - 1.0))` + `pnl_target_coefficient = 1.0 + (win_reward_factor · effective_rr) · tanh(pnl_amplification_sensitivity · (loss_ratio - 1.0))` - Else: `pnl_target_coefficient = 1.0` ##### Efficiency @@ -289,6 +295,16 @@ Let `max_u = max_unrealized_profit`, `min_u = min_unrealized_profit`, `efficiency_coefficient = 1 + efficiency_weight · (efficiency_center - ratio)` - Else: `efficiency_coefficient = 1` +In synthetic `unrealized_pnl` mode, sampled market prices accumulate each +candle's return independently of the transformed, retained price. The +fee-aware sampled PnL is bounded, scaled by the duration-dependent tanh +factor, and converted to the retained price. Exit-efficiency extrema start +with the fee-adjusted PnL at the entry fill and then include each retained +mark; a sampled candidate is never retained as an extremum. Synthetic marks +are capped at +0.15; their lower bound is the lesser of -0.15 and the +fee-adjusted entry PnL. The extreme-PnL check permits that fee loss and +floating-point roundoff at its boundary, but rejects larger excursions. + ##### Exit Attenuation | Parameter | Default | Description | @@ -451,6 +467,10 @@ Flags hierarchy: Auto-skip if `num_samples < 4`. +Reusing an output directory removes only stale analyzer-owned +`feature_importance.csv` and the three `partial_dependence_{trade_duration,idle_duration,pnl}.csv` +files before writing the new report; unrelated files are retained. + ### Reproducibility | Component | Controlled By | Notes | @@ -535,6 +555,11 @@ descriptive. | `partial_dependence_*.csv` | Partial dependence data | | `manifest.json` | Runtime manifest (simulation + reward params + hash) | +The `sample_entry_prob`, `sample_exit_prob`, and `sample_neutral_prob` columns in +`reward_samples.csv` report marginal probabilities of valid actions when applicable. +With masking disabled, these probabilities include the 90% valid-action mass; +they are not conditional on drawing a valid action. + ### Manifest (`manifest.json`) | Field | Type | Description | @@ -563,8 +588,14 @@ Within the same analyzer revision, identical `params_hash` values mean the resol | `*_ks_statistic` | KS two-sample statistic | [0,1]; higher ⇒ divergence | | `*_ks_pvalue` | KS test p-value | API-only with `independent_observations=True`; omitted by the descriptive CLI | -Implementation: 50-bin histograms with ε=1e-10; constants have zero divergence. -Inferential KS p-values are available only under the programmatic independence contract. +Implementation: up to 50 evenly spaced histogram edges (normally 49 bins) with +ε=1e-10; constants have zero divergence. + +Non-finite numeric values in real episodes are marked missing. Each feature is +compared only when both synthetic and real data contain at least 10 finite +observations; otherwise it is omitted. When none qualify, the report distinguishes +missing episodes, no comparable finite data, and fewer than 10 finite observations +per dataset and comparable feature. --- @@ -603,8 +634,8 @@ uv run python reward_space_analysis.py \ --out_dir real_vs_synthetic ``` -Shift metrics: lower divergence preferred (except p-value: higher ⇒ cannot -reject equality). +For the CLI's descriptive shift metrics, lower values indicate closer +synthetic and real distributions. ### Batch Analysis diff --git a/ReforceXY/reward_space_analysis/reward_space_analysis.py b/ReforceXY/reward_space_analysis/reward_space_analysis.py index 960d072..c4f6854 100644 --- a/ReforceXY/reward_space_analysis/reward_space_analysis.py +++ b/ReforceXY/reward_space_analysis/reward_space_analysis.py @@ -1545,6 +1545,7 @@ _SAMPLE_DURATION_HAZARD_OVERTIME_MULTIPLIER = 4.0 _SAMPLE_DURATION_HAZARD_MAX_PROBABILITY = 0.9 _SAMPLE_EXIT_PROBABILITY_MIN = 0.002 _SAMPLE_EXIT_PROBABILITY_MAX = 0.2 +_SAMPLE_INVALID_ACTION_PROBABILITY = 0.1 def _sampling_probabilities( @@ -1595,6 +1596,7 @@ def _sample_action( max_trade_duration_candles: int, idle_duration: int, max_idle_duration_candles: int, + action_masking: bool = True, ) -> tuple[Actions, float, float, float]: entry_prob, exit_prob, neutral_prob = _sampling_probabilities( position, @@ -1612,15 +1614,29 @@ def _sample_action( else: choices = [Actions.Neutral, Actions.Long_enter] weights = [neutral_prob, entry_prob] - action = rng.choices(choices, weights=weights, k=1)[0] - return action, entry_prob, exit_prob, neutral_prob - - if position == Positions.Long: + elif position == Positions.Long: choices = [Actions.Neutral, Actions.Long_exit] + weights = [1.0 - exit_prob, exit_prob] else: # Positions.Short choices = [Actions.Neutral, Actions.Short_exit] + weights = [1.0 - exit_prob, exit_prob] + + if not action_masking: + invalid_choices = [ + candidate + for candidate in Actions + if not _is_valid_action(position, candidate, short_allowed=short_allowed) + ] + valid_mass = 1.0 - _SAMPLE_INVALID_ACTION_PROBABILITY + weights = [weight * valid_mass for weight in weights] + choices.extend(invalid_choices) + weights.extend( + [_SAMPLE_INVALID_ACTION_PROBABILITY / len(invalid_choices)] * len(invalid_choices) + ) + entry_prob *= valid_mass + exit_prob *= valid_mass + neutral_prob *= valid_mass - weights = [1.0 - exit_prob, exit_prob] action = rng.choices(choices, weights=weights, k=1)[0] return action, entry_prob, exit_prob, neutral_prob @@ -1735,9 +1751,11 @@ def simulate_samples( pnl = 0.0 max_unrealized_profit = 0.0 min_unrealized_profit = 0.0 + pnl_floor = -0.15 # Synthetic market state current_open = 1.0 + sampled_open = current_open entry_open = current_open for _ in range(num_samples): @@ -1749,6 +1767,7 @@ def simulate_samples( max_trade_duration_candles=max_trade_duration_candles, idle_duration=idle_duration, max_idle_duration_candles=max_idle_duration_candles, + action_masking=action_masking, ) context = RewardContext( @@ -1761,24 +1780,22 @@ def simulate_samples( action=action, ) + next_position = _get_next_position(position, action, short_allowed=short_allowed) if position == Positions.Neutral: - if action == Actions.Long_enter: - position = Positions.Long + if next_position != Positions.Neutral: + position = next_position trade_duration = 0 idle_duration = 0 entry_open = current_open - max_unrealized_profit = -np.inf - min_unrealized_profit = np.inf - elif action == Actions.Short_enter and short_allowed: - position = Positions.Short - trade_duration = 0 - idle_duration = 0 - entry_open = current_open - max_unrealized_profit = -np.inf - min_unrealized_profit = np.inf + entry_pnl = _compute_unrealized_pnl_estimate( + position, entry_open=entry_open, current_open=entry_open, params=params + ) + max_unrealized_profit = min_unrealized_profit = entry_pnl + sampled_open = entry_open + pnl_floor = min(-0.15, entry_pnl) else: idle_duration = 0 - if action in (Actions.Long_exit, Actions.Short_exit): + if next_position == Positions.Neutral: position = Positions.Neutral trade_duration = 0 idle_duration = 0 @@ -1807,33 +1824,36 @@ def simulate_samples( step_return = 0.0 step_return = float(np.clip(step_return, -0.95, 0.95)) - current_open = float(max(1e-6, current_open * (1.0 + step_return))) - # Always sample the random-walk price so both modes consume the same RNG stream. - # Unrealized-PnL mode replaces it below with the fee-aware price implied by - # the target PnL before reward calculation. + # Sample once in both modes; keep the raw market path separate from the + # transformed price while a position is open. + transform_unrealized_pnl = position in ( + Positions.Long, + Positions.Short, + ) and _get_bool_param(params, "unrealized_pnl", False) + if transform_unrealized_pnl: + sampled_open = float(max(1e-6, sampled_open * (1.0 + step_return))) + candidate_open = sampled_open + else: + current_open = float(max(1e-6, current_open * (1.0 + step_return))) + candidate_open = current_open if position in (Positions.Long, Positions.Short): candidate_pnl = float( np.clip( _compute_unrealized_pnl_estimate( position, entry_open=entry_open, - current_open=current_open, + current_open=candidate_open, params=params, ), - -0.15, + pnl_floor, 0.15, ) ) - if _get_bool_param(params, "unrealized_pnl", False): - # Let the sampled market move shape the next retained PnL without - # storing the discarded candidate in exit-efficiency extrema. - prospective_max = max(max_unrealized_profit, candidate_pnl) - prospective_min = min(min_unrealized_profit, candidate_pnl) - center_unrealized = 0.5 * (prospective_max + prospective_min) + if transform_unrealized_pnl: beta = _get_float_param(params, "pnl_amplification_sensitivity") hold_ratio = _compute_duration_ratio(trade_duration, max_trade_duration_candles) target_pnl = float( - np.clip(center_unrealized * math.tanh(beta * hold_ratio), -0.15, 0.15) + np.clip(candidate_pnl * math.tanh(beta * hold_ratio), pnl_floor, 0.15) ) entry_fee_rate, exit_fee_rate = _get_fee_rates(params) if position == Positions.Long: @@ -1853,7 +1873,7 @@ def simulate_samples( current_open=current_open, params=params, ), - -0.15, + pnl_floor, 0.15, ) ) @@ -1938,7 +1958,11 @@ def simulate_samples( "reward_base": breakdown.base_reward, "reward_pbrs_delta": breakdown.pbrs_delta, "reward_invariance_correction": breakdown.invariance_correction, - "is_invalid": float(breakdown.invalid_penalty != 0.0), + "is_invalid": float( + not _is_valid_action( + context.position, context.action, short_allowed=short_allowed + ) + ), "pbrs_invariant": bool(pbrs_invariant), } ) @@ -1947,36 +1971,52 @@ def simulate_samples( df.attrs["reward_params"] = source_params # Validate critical algorithmic invariants - _validate_simulation_invariants(df) + _validate_simulation_invariants(df, params) return df -def _validate_simulation_invariants(df: pd.DataFrame) -> None: +def _validate_simulation_invariants(df: pd.DataFrame, params: RewardParams) -> None: """Fail fast if simulation violates action/state invariants.""" eps_pnl = float(INTERNAL_GUARDS["sim_zero_pnl_epsilon"]) eps_reward = float(INTERNAL_GUARDS["sim_zero_reward_epsilon"]) - thr_extreme = float(INTERNAL_GUARDS["sim_extreme_pnl_threshold"]) - - # INVARIANT 1: Action-position compatibility - long_exits = df[(df["action"] == 2.0) & (df["position"] != 1.0)] - short_exits = df[(df["action"] == 4.0) & (df["position"] != 0.0)] - if len(long_exits) > 0: - raise AssertionError(f"Sim: {len(long_exits)} Long_exit actions without Long position") - if len(short_exits) > 0: - raise AssertionError(f"Sim: {len(short_exits)} Short_exit actions without Short position") - - long_entries = df[(df["action"] == 1.0) & (df["position"] != 0.5)] - short_entries = df[(df["action"] == 3.0) & (df["position"] != 0.5)] - if len(long_entries) > 0: - raise AssertionError( - f"Sim: {len(long_entries)} Long_enter actions without Neutral position" - ) - if len(short_entries) > 0: - raise AssertionError( - f"Sim: {len(short_entries)} Short_enter actions without Neutral position" - ) + entry_fee_rate, exit_fee_rate = _get_fee_rates(params) + thr_extreme = max( + float(INTERNAL_GUARDS["sim_extreme_pnl_threshold"]), + (1.0 + entry_fee_rate) * (1.0 + exit_fee_rate) - 1.0, + ) + + # INVARIANT 1: masked sampling must only emit legal actions; unmasked + # actions may be invalid but cannot change position before terminal liquidation. + if _get_bool_param(params, "action_masking", True): + long_exits = df[(df["action"] == 2.0) & (df["position"] != 1.0)] + short_exits = df[(df["action"] == 4.0) & (df["position"] != 0.0)] + if len(long_exits) > 0: + raise AssertionError(f"Sim: {len(long_exits)} Long_exit actions without Long position") + if len(short_exits) > 0: + raise AssertionError( + f"Sim: {len(short_exits)} Short_exit actions without Short position" + ) + + long_entries = df[(df["action"] == 1.0) & (df["position"] != 0.5)] + short_entries = df[(df["action"] == 3.0) & (df["position"] != 0.5)] + if len(long_entries) > 0: + raise AssertionError( + f"Sim: {len(long_entries)} Long_enter actions without Neutral position" + ) + if len(short_entries) > 0: + raise AssertionError( + f"Sim: {len(short_entries)} Short_enter actions without Neutral position" + ) + else: + changed_on_invalid = df[ + df["is_invalid"].eq(1.0) + & ~df["terminated"].eq(True) + & df["next_position"].ne(df["position"]) + ] + if len(changed_on_invalid) > 0: + raise AssertionError(f"Sim: {len(changed_on_invalid)} invalid actions changed position") # INVARIANT 2: Duration logic neutral_with_trade = df[(df["position"] == 0.5) & (df["trade_duration"] > 0)] @@ -1997,15 +2037,15 @@ def _validate_simulation_invariants(df: pd.DataFrame) -> None: raise AssertionError(f"Sim: {len(neutral_with_pnl)} Neutral positions with non-zero pnl") # Economic exits belong to voluntary exits or a proven terminal liquidation. + voluntary_exit = ( + df["position"].eq(Positions.Long.value) & df["action"].eq(Actions.Long_exit.value) + ) | (df["position"].eq(Positions.Short.value) & df["action"].eq(Actions.Short_exit.value)) liquidation = df.get("terminal_liquidation", pd.Series(False, index=df.index)).eq(True) valid_liquidation = ( df.get("terminated", pd.Series(False, index=df.index)).eq(True) & df.get("next_position", pd.Series(np.nan, index=df.index)).eq(Positions.Neutral.value) & ( - ( - df["position"].isin([Positions.Long.value, Positions.Short.value]) - & ~df["action"].isin([Actions.Long_exit.value, Actions.Short_exit.value]) - ) + (df["position"].isin([Positions.Long.value, Positions.Short.value]) & ~voluntary_exit) | ( df["position"].eq(Positions.Neutral.value) & df["action"].isin([Actions.Long_enter.value, Actions.Short_enter.value]) @@ -2016,7 +2056,7 @@ def _validate_simulation_invariants(df: pd.DataFrame) -> None: if (liquidation & ~valid_liquidation).any(): raise AssertionError("Sim: terminal liquidation lacks a terminal open-position transition") non_exit_with_exit_reward = df[ - (~df["action"].isin([2.0, 4.0])) & ~liquidation & (df["reward_exit"].abs() > eps_reward) + ~voluntary_exit & ~liquidation & (df["reward_exit"].abs() > eps_reward) ] if len(non_exit_with_exit_reward) > 0: raise AssertionError( @@ -2024,7 +2064,8 @@ def _validate_simulation_invariants(df: pd.DataFrame) -> None: ) # INVARIANT 5: Bounded values - extreme_pnl = df[(df["pnl"].abs() > thr_extreme)] + # Fee-adjusted prices can differ from the nominal fee product by a few ULPs. + extreme_pnl = df[df["pnl"].abs() > thr_extreme + eps_pnl] if len(extreme_pnl) > 0: max_abs_pnl = float(df["pnl"].abs().max()) raise AssertionError( @@ -2563,6 +2604,15 @@ def load_real_episodes( RewardDiagnosticsWarning, stacklevel=2, ) + infinite = df[col].isin((np.inf, -np.inf)) + count_infinite = int(infinite.sum()) + if count_infinite: + df.loc[infinite, col] = np.nan + warnings.warn( + f"Data: replaced {count_infinite} non-finite value(s) in column '{col}' with NaN when loading '{path}'", + RewardDiagnosticsWarning, + stacklevel=2, + ) # Ensure required columns exist (or fill with NaN if allowed) required = { @@ -2606,6 +2656,10 @@ def _stabilize_divergence(value: float, *, bin_count: int, metric_name: str) -> return value +_DISTRIBUTION_SHIFT_FEATURES: Final[tuple[str, ...]] = ("pnl", "trade_duration", "idle_duration") +_MIN_DISTRIBUTION_SHIFT_OBSERVATIONS: Final[int] = 10 + + def compute_distribution_shift_metrics( synthetic_df: pd.DataFrame, real_df: pd.DataFrame, @@ -2621,13 +2675,19 @@ def compute_distribution_shift_metrics( reported. Constants yield exact zero distances and, inferentially, p=1.0. """ metrics = {} - continuous_features = ["pnl", "trade_duration", "idle_duration"] + continuous_features = _DISTRIBUTION_SHIFT_FEATURES for feature in continuous_features: - synth_values = synthetic_df[feature].dropna().values - real_values = real_df[feature].dropna().values - - if len(synth_values) < 10 or len(real_values) < 10: + synth_values = synthetic_df[feature].to_numpy(dtype=float, na_value=np.nan) + real_values = real_df[feature].to_numpy(dtype=float, na_value=np.nan) + synth_finite = np.isfinite(synth_values) + real_finite = np.isfinite(real_values) + if not synth_finite.all(): + synth_values = synth_values[synth_finite] + if not real_finite.all(): + real_values = real_values[real_finite] + + if min(len(synth_values), len(real_values)) < _MIN_DISTRIBUTION_SHIFT_OBSERVATIONS: continue min_val = min(synth_values.min(), real_values.min()) @@ -2817,7 +2877,7 @@ def statistical_hypothesis_tests( u_stat, p_val = stats.mannwhitneyu(pnl_positive, pnl_negative) n1, n2 = len(pnl_positive), len(pnl_negative) - rb = 1.0 - 2.0 * (float(u_stat) / float(n1 * n2)) if n1 > 0 and n2 > 0 else np.nan + rb = 2.0 * (float(u_stat) / float(n1 * n2)) - 1.0 if n1 > 0 and n2 > 0 else np.nan if np.isfinite(rb): rb = float(np.clip(rb, -1.0, 1.0)) @@ -2946,6 +3006,8 @@ def bootstrap_confidence_intervals( """ if independent_observations is not True: raise ValueError("Stats: bootstrap intervals require independent_observations=True") + if n_bootstrap < 1: + raise ValueError("Stats: n_bootstrap must be positive") alpha = 1 - confidence_level lower_percentile = 100 * alpha / 2 upper_percentile = 100 * (1 - alpha / 2) @@ -3713,8 +3775,8 @@ def build_argument_parser() -> argparse.ArgumentParser: parser.add_argument( "--base_factor", type=float, - default=100.0, - help="Base reward scaling factor (default: 100).", + default=None, + help=f"Base reward scaling factor (default: {DEFAULT_MODEL_REWARD_PARAMETERS['base_factor']:g}).", ) parser.add_argument( "--profit_aim", @@ -3878,6 +3940,10 @@ def write_complete_statistical_analysis( ) -> None: """Generate a single comprehensive statistical analysis report.""" output_dir.mkdir(parents=True, exist_ok=True) + # These names belong to this report; leave all other user files untouched. + (output_dir / "feature_importance.csv").unlink(missing_ok=True) + for feature in ("trade_duration", "idle_duration", "pnl"): + (output_dir / f"partial_dependence_{feature}.csv").unlink(missing_ok=True) report_path = output_dir / "statistical_analysis.md" reward_params: RewardParams = ( @@ -4039,6 +4105,23 @@ def write_complete_statistical_analysis( distribution_shift = compute_distribution_shift_metrics( df, real_df, independent_observations=independent_observations ) + distribution_shift_unavailable = ( + "no real episodes provided" if real_df is None else "no comparable finite observations" + ) + + if ( + real_df is not None + and not distribution_shift + and any( + np.isfinite(df[feature].to_numpy(dtype=float, na_value=np.nan)).any() + and np.isfinite(real_df[feature].to_numpy(dtype=float, na_value=np.nan)).any() + for feature in _DISTRIBUTION_SHIFT_FEATURES + ) + ): + distribution_shift_unavailable = ( + "insufficient finite observations; " + f"at least {_MIN_DISTRIBUTION_SHIFT_OBSERVATIONS} per dataset and feature required" + ) # Write comprehensive report with report_path.open("w", encoding="utf-8") as f: @@ -4606,7 +4689,7 @@ def write_complete_statistical_analysis( else: # Placeholder keeps numbering stable and explicit f.write("### 5.4 Distribution Shift Analysis\n\n") - f.write("_Not performed (no real episodes provided)._\n\n") + f.write(f"_Not performed ({distribution_shift_unavailable})._\n\n") # Footer f.write("---\n\n") @@ -4636,7 +4719,9 @@ def write_complete_statistical_analysis( if distribution_shift: f.write("6. **Distribution Shift** - Comparison with real trading data\n") else: - f.write("6. **Distribution Shift** - Not performed (no real episodes provided)\n") + f.write( + f"6. **Distribution Shift** - Not performed ({distribution_shift_unavailable})\n" + ) if invariance_status is not None: f.write("7. **PBRS Invariance** - " + invariance_status + "\n") f.write("\n") @@ -4672,7 +4757,7 @@ def main() -> None: print("CLI: Parameter adjustments applied\n" + "\n".join(adj_lines)) # Effective values: defaults < explicit flags < --params, resolved once. - base_factor = _get_float_param(params, "base_factor", float(args.base_factor)) + base_factor = _get_float_param(params, "base_factor") profit_aim = _get_float_param(params, "profit_aim", float(args.profit_aim)) risk_reward_ratio = _get_float_param(params, "risk_reward_ratio", float(args.risk_reward_ratio)) effective_params = { diff --git a/ReforceXY/reward_space_analysis/tests/README.md b/ReforceXY/reward_space_analysis/tests/README.md index 82733cf..4b0bc00 100644 --- a/ReforceXY/reward_space_analysis/tests/README.md +++ b/ReforceXY/reward_space_analysis/tests/README.md @@ -35,8 +35,9 @@ Markers are declared in `pyproject.toml` and enforced with `--strict-markers`. ## Test Framework -The test suite uses **pytest as the runner** with **unittest.TestCase as the -base class** (via `RewardSpaceTestBase`). +The suite runs under **pytest**. Most class-based tests inherit from +`RewardSpaceTestBase` (a `unittest.TestCase` subclass); others are standalone +pytest functions. ### Hybrid Approach Rationale @@ -51,7 +52,7 @@ This design provides: ### Base Class -All test classes inherit from `RewardSpaceTestBase` (defined in `test_base.py`): +Class-based tests normally inherit from `RewardSpaceTestBase` (in `test_base.py`): ```python from ..test_base import RewardSpaceTestBase @@ -172,68 +173,80 @@ uv run --locked --extra dev pytest -m "statistics and slow" -q Columns: -- ID: Stable identifier (`--NNN`). +- ID: Stable identifier (`--NNN`; optional letter for + split invariants). - Category: Taxonomy directory marker. - Description: Concise invariant statement. -- Owning File: Path:line of primary declaration (prefer comment line - `# Owns invariant:` when present; otherwise docstring line). -- Notes: Clarifications (sub-modes, extensions, non-owning references elsewhere, - line clusters for multi-path coverage). - -| ID | Category | Description | Owning File | Notes | -| --------------------------------------------- | ----------- | ----------------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| report-additives-deterministic-092 | components | Additives deterministic report section | components/test_additives.py:4 | Integration/PBRS may reference outcome non-owning | -| robustness-decomposition-integrity-101 | robustness | Single active core component equals total reward under mutually exclusive scenarios | robustness/test_robustness.py:43 | Scenarios: idle, hold, exit, invalid; non-owning refs integration/test_reward_calculation.py | -| robustness-exit-mode-fallback-102 | robustness | Unknown exit_attenuation_mode falls back to linear w/ warning | robustness/test_robustness.py:654 | Comment line (function at :655) | -| robustness-negative-grace-clamp-103 | robustness | Negative exit_plateau_grace clamps to 0.0 w/ warning | robustness/test_robustness.py:696 | | -| robustness-invalid-power-tau-104 | robustness | Invalid power tau falls back alpha=1.0 w/ warning | robustness/test_robustness.py:747 | | -| robustness-near-zero-half-life-105 | robustness | Near-zero half life yields no attenuation (factor≈base) | robustness/test_robustness.py:792 | | -| pbrs-canonical-exit-semantic-106 | pbrs | Canonical exit uses shaping=-prev_potential and next_potential=0.0 | pbrs/test_pbrs.py:374 | Uses stored potential across steps; no drift correction applied | -| statistics-partial-deps-skip-107 | statistics | skip_partial_dependence => empty PD structures | statistics/test_statistics.py:42 | Docstring line | -| helpers-transitions-preserve-multiplicity-108 | helpers | Repeated transitions retain their empirical multiplicity | helpers/test_utilities.py:26 | | -| helpers-missing-cols-fill-109 | helpers | Missing required columns filled with NaN + single warning | helpers/test_utilities.py:51 | Docstring line | -| statistics-binned-stats-min-edges-110 | statistics | <2 bin edges raises ValueError | statistics/test_statistics.py:60 | Docstring line | -| statistics-constant-cols-exclusion-111 | statistics | Constant columns excluded & listed | statistics/test_statistics.py:71 | Docstring line | -| statistics-degenerate-distribution-shift-112 | statistics | Constants: zero distances; KS p only with declared independent observations | statistics/test_statistics.py:87 | Docstring line | -| statistics-constant-dist-exact-ci-113a | statistics | Both modes retain exact constant CI bounds | statistics/test_statistics.py:606 | | -| statistics-percentile-outside-mean-113b | statistics | Percentile bounds need not contain the sample mean | statistics/test_statistics.py:620 | | -| statistics-constant-diagnostics-115 | statistics | Constants have N/A higher moments, normality tests and Q-Q fits in both modes | statistics/test_statistics.py:194 | | -| pbrs-canonical-near-zero-report-116 | pbrs | Canonical trajectories with valid evidence are classified as verified | pbrs/test_pbrs.py:1509 | Requires local identity, continuity, discounted terminal boundary, and zero observed additives; the non-owning boundary test also covers a complete singleton terminal episode | -| robustness-exit-pnl-only-117 | robustness | Only exit actions have non-zero PnL | robustness/test_robustness.py:127 | Comment line | -| pbrs-absence-shift-placeholder-118 | pbrs | Placeholder shift line present when shaping shift is absent | pbrs/test_pbrs.py:1877 | | -| components-pbrs-breakdown-fields-119 | components | PBRS breakdown fields finite and mathematically aligned | components/test_reward_components.py:830 | Tests base_reward, pbrs_delta and invariance_correction alignment | -| integration-pbrs-metrics-section-120 | integration | PBRS Metrics section present in report with tracing metrics | integration/test_report_formatting.py:137 | | -| cli-pbrs-csv-columns-121 | cli | PBRS columns in reward_samples.csv when shaping enabled | cli/test_cli_params_and_csv.py:347 | Verifies finite reward_base, reward_pbrs_delta and reward_invariance_correction values | -| statistics-bh-finite-family-122 | statistics | Undefined tests excluded from finite-only BH family; marked non-applicable | statistics/test_statistics.py:499 | | -| statistics-independence-contract-123 | statistics | Inferential helpers require independent_observations=True | statistics/test_statistics.py:636 | Covers hypothesis tests and bootstrap intervals | -| report-independent-sections-124 | integration | CI, diagnostics and shift sections do not depend on hypothesis-test output | integration/test_report_formatting.py:28 | Also verifies the reported bootstrap resample count | -| pbrs-discounted-evidence-125 | pbrs | Verification requires local identity, continuity and discounted terminal boundary | pbrs/test_pbrs.py:1699 | Discontinuous potentials are not verified | -| statistics-proportional-histograms-126 | statistics | Proportional histograms ignore sample count; moved mass yields positive KL/JS | statistics/test_statistics.py:119 | KL and JS remain finite and non-negative | -| pbrs-near-bound-clamp-127 | pbrs | Relaxed near-bound clamps apply exact endpoints and retain all reasons | pbrs/test_pbrs.py:1276 | Includes numeric-string coercion | -| pbrs-exit-mode-validation-128 | pbrs | Exit-potential choices are strict or canonicalized; direct calls fail safe | pbrs/test_pbrs.py:1296 | Direct calculation and simulation boundaries warn before fallback; PBRS calls suppress additives | -| cli-invalid-exit-mode-129 | cli | Invalid `--params exit_potential_mode` fails before artifacts | cli/test_cli_params_and_csv.py:322 | Strict CLI validation | -| cli-warning-header-recognition-130 | cli | Warning counts accept only anchored Python warning header formats | cli/test_cli_params_and_csv.py:49 | Covers POSIX, relative, synthetic and Windows source locations | -| pbrs-invalid-mode-provenance-131 | pbrs | Invalid imported exit-mode metadata cannot certify canonical invariance | pbrs/test_pbrs.py:475 | Preserves the invalid raw value and reports effective additive settings as unknown | +- Owning test: Relative source path and function name (`path.py::test_name`), + stable across unrelated line insertions. +- Notes: Sub-modes, non-owning references and multi-path coverage. + +| ID | Category | Description | Owning test | Notes | +| --------------------------------------------- | ----------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| report-additives-deterministic-092 | components | Additives deterministic report section | components/test_additives.py::test_additive_activation_deterministic_contribution | Integration/PBRS may reference outcome non-owning | +| robustness-decomposition-integrity-101 | robustness | Single active core component equals total reward under mutually exclusive scenarios | robustness/test_robustness.py::test_decomposition_integrity | Scenarios: idle, hold, exit, invalid; non-owning refs integration/test_reward_calculation.py | +| robustness-exit-mode-fallback-102 | robustness | Unknown exit_attenuation_mode falls back to linear w/ warning | robustness/test_robustness.py::test_robustness_102_unknown_exit_mode_fallback_linear | | +| robustness-negative-grace-clamp-103 | robustness | Negative exit_plateau_grace clamps to 0.0 w/ warning | robustness/test_robustness.py::test_robustness_103_negative_plateau_grace_clamped | | +| robustness-invalid-power-tau-104 | robustness | Invalid power tau falls back alpha=1.0 w/ warning | robustness/test_robustness.py::test_robustness_104_invalid_power_tau_fallback_alpha_one | | +| robustness-near-zero-half-life-105 | robustness | Near-zero half life yields no attenuation (factor≈base) | robustness/test_robustness.py::test_robustness_105_half_life_near_zero_fallback | | +| pbrs-canonical-exit-semantic-106 | pbrs | Canonical exit uses shaping=-prev_potential and next_potential=0.0 | pbrs/test_pbrs.py::test_exit_step_shaping_matches_exit_step_rules | Uses stored potential across steps; no drift correction applied | +| statistics-partial-deps-skip-107 | statistics | skip_partial_dependence => empty PD structures | statistics/test_statistics.py::test_statistics_feature_analysis_skip_partial_dependence | | +| helpers-transitions-preserve-multiplicity-108 | helpers | Repeated transitions retain their empirical multiplicity | helpers/test_utilities.py::test_repeated_transitions_preserve_multiplicity | | +| helpers-missing-cols-fill-109 | helpers | Missing required columns filled with NaN + single warning | helpers/test_utilities.py::test_missing_multiple_required_columns_single_warning | | +| statistics-binned-stats-min-edges-110 | statistics | <2 bin edges raises ValueError | statistics/test_statistics.py::test_statistics_binned_stats_invalid_bins_raises | Docstring line | +| statistics-constant-cols-exclusion-111 | statistics | Constant columns excluded & listed | statistics/test_statistics.py::test_statistics_correlation_dropped_constant_columns | Docstring line | +| statistics-degenerate-distribution-shift-112 | statistics | Constants: zero distances; KS p only with declared independent observations | statistics/test_statistics.py::test_statistics_distribution_shift_metrics_degenerate_zero | Docstring line | +| statistics-constant-dist-exact-ci-113a | statistics | Both modes retain exact constant CI bounds | statistics/test_statistics.py::test_stats_bootstrap_constant_distribution_exact_bounds | | +| statistics-percentile-outside-mean-113b | statistics | Percentile bounds need not contain the sample mean | statistics/test_statistics.py::test_stats_bootstrap_percentiles_need_not_contain_mean | | +| statistics-constant-diagnostics-115 | statistics | Constants have N/A higher moments, normality tests and Q-Q fits in both modes | statistics/test_statistics.py::test_statistics_distribution_constant_diagnostics | | +| pbrs-canonical-near-zero-report-116 | pbrs | Canonical trajectories with valid evidence are classified as verified | pbrs/test_pbrs.py::test_pbrs_canonical_near_zero_report | Requires local identity, continuity, discounted terminal boundary, and zero observed additives; the non-owning boundary test also covers a complete singleton terminal episode | +| robustness-exit-pnl-only-117 | robustness | Only exit actions have non-zero PnL | robustness/test_robustness.py::test_pnl_invariant_exit_only | | +| pbrs-absence-shift-placeholder-118 | pbrs | Placeholder shift line present when shaping shift is absent | pbrs/test_pbrs.py::test_pbrs_absence_and_distribution_shift_placeholder | | +| components-pbrs-breakdown-fields-119 | components | PBRS breakdown fields finite and mathematically aligned | components/test_reward_components.py::test_pbrs_breakdown_fields_finite_and_aligned | Tests base_reward, pbrs_delta and invariance_correction alignment | +| integration-pbrs-metrics-section-120 | integration | PBRS Metrics section present in report with tracing metrics | integration/test_report_formatting.py::test_report_includes_pbrs_metrics_section | | +| cli-pbrs-csv-columns-121 | cli | PBRS columns in reward_samples.csv when shaping enabled | cli/test_cli_params_and_csv.py::test_csv_contains_pbrs_columns_when_shaping_present | Verifies finite reward_base, reward_pbrs_delta and reward_invariance_correction values | +| statistics-bh-finite-family-122 | statistics | Undefined tests excluded from finite-only BH family; marked non-applicable | statistics/test_statistics.py::test_bh_excludes_undefined_tests_from_finite_family | | +| statistics-independence-contract-123 | statistics | Inferential helpers require independent_observations=True | statistics/test_statistics.py::test_inference_helpers_require_independent_observations | Covers hypothesis tests and bootstrap intervals | +| report-independent-sections-124 | integration | CI, diagnostics and shift sections do not depend on hypothesis-test output | integration/test_report_formatting.py::test_statistical_sections_do_not_depend_on_hypothesis_tests | Also verifies the reported bootstrap resample count | +| pbrs-discounted-evidence-125 | pbrs | Verification requires local identity, continuity and discounted terminal boundary | pbrs/test_pbrs.py::test_pbrs_canonical_discontinuous_potentials_report | Discontinuous potentials are not verified | +| statistics-proportional-histograms-126 | statistics | Proportional histograms ignore sample count; moved mass yields positive KL/JS | statistics/test_statistics.py::test_distribution_shift_proportional_histograms_ignore_sample_count | KL and JS remain finite and non-negative | +| pbrs-near-bound-clamp-127 | pbrs | Relaxed near-bound clamps apply exact endpoints and retain all reasons | pbrs/test_pbrs.py::test_validate_reward_parameters_records_near_bound_clamps_exactly | Includes numeric-string coercion | +| pbrs-exit-mode-validation-128 | pbrs | Exit-potential choices are strict or canonicalized; direct calls fail safe | pbrs/test_pbrs.py::test_invalid_exit_mode_warns_at_direct_and_simulation_boundaries | Direct calculation and simulation boundaries warn before fallback; PBRS calls suppress additives | +| cli-invalid-exit-mode-129 | cli | Invalid `--params exit_potential_mode` fails before artifacts | cli/test_cli_params_and_csv.py::test_invalid_exit_potential_mode_params_fails_before_artifacts | Strict CLI validation | +| cli-warning-header-recognition-130 | cli | Warning counts accept only anchored Python warning header formats | cli/test_cli_params_and_csv.py::test_warning_header_positive_and_negative_formats | Covers POSIX, relative, synthetic and Windows source locations | +| pbrs-invalid-mode-provenance-131 | pbrs | Invalid imported exit-mode metadata cannot certify canonical invariance | pbrs/test_pbrs.py::test_report_rejects_invalid_exit_mode_provenance | Preserves the invalid raw value and reports effective additive settings as unknown | +| pbrs-synthetic-fee-floor-132 | pbrs | High-fee entry loss remains in long/short synthetic PnL and exit rewards | pbrs/test_pbrs.py::test_synthetic_fee_loss_extrema_match_retained_pnl | Direct and transformed trajectories | +| pbrs-synthetic-profitable-mark-133 | pbrs | Favorable long/short marks remain profitable after high-fee synthetic transformation | pbrs/test_pbrs.py::test_synthetic_high_fee_winner_retains_profitable_mark | Covers immediate gains and recovery after an initial loss on the independent sampled market path | +| pbrs-synthetic-fee-boundary-134 | pbrs | Fee-boundary rounding is accepted while materially extreme PnL is rejected | pbrs/test_pbrs.py::test_synthetic_high_fee_short_boundary_rejects_real_excess | Non-unit entry price exposes floating-point roundoff | +| pbrs-synthetic-latent-price-135 | pbrs | Clipped candidate PnL does not erase the sampled market path for later holds | pbrs/test_pbrs.py::test_unrealized_pnl_retains_sampled_market_path_after_candidate_cap | Equal first retained marks from distinct market prices diverge after the same adverse return | +| api-unmasked-invalid-actions-136 | api | Unmasked invalid actions keep positions and receive penalties | api/test_api_helpers.py::test_unmasked_simulation_samples_invalid_actions_without_changing_position | Includes spot, futures and zero-penalty invalid actions | +| api-terminal-invalid-exit-137 | api | Wrong-side terminal exits liquidate the held side | api/test_api_helpers.py::test_invalid_terminal_exit_liquidates_the_held_position | Prevents dropped terminal trades | +| cli-stale-generated-artifacts-138 | cli | Skipped analyses remove only reserved outputs from an earlier run | cli/test_cli_params_and_csv.py::test_skipped_analysis_removes_only_stale_generated_artifacts | Preserves unrelated files | +| helpers-nonfinite-episodes-139 | helpers | Infinite real observations become missing without dropping rows | helpers/test_utilities.py::test_nonfinite_numeric_episodes_are_marked_missing | Retains transition multiplicity | +| statistics-finite-shift-140 | statistics | Finite observations remain comparable despite an infinite value | statistics/test_statistics.py::test_distribution_shift_uses_finite_observations | Rejects all-nonfinite features | +| statistics-rank-direction-141 | statistics | Rank-biserial effect follows the named first-group advantage | statistics/test_statistics.py::test_pnl_rank_biserial_direction_matches_named_first_group | Checks both directions | +| statistics-bootstrap-count-142 | statistics | Zero and negative resample counts fail for variable and constant data | statistics/test_statistics.py::test_bootstrap_rejects_nonpositive_resample_count | Rejects missing bootstrap | +| api-unmasked-sample-probabilities-144 | api | Unmasked sample probabilities match marginal valid-action frequencies | api/test_api_helpers.py::test_unmasked_sampling_probabilities_match_action_frequencies | Spot/futures entries, long/short exits, and neutral probability | ### Non-Owning Smoke / Reference Checks -Files that reference invariant outcomes (formatting, aggregation) without owning -the invariant must include a leading comment: +Tests that check an invariant owned elsewhere identify its owner in a comment +or docstring, for example: ```python # Non-owning smoke; ownership: ``` -Table tracks approximate line ranges and source ownership: +The following tests also check outcomes owned elsewhere: -| File | Lines (approx) | References | Ownership Source | -| -------------------------------------- | -------------- | ---------------------------------------------------- | ------------------------------------------------------------------- | -| integration/test_reward_calculation.py | 44 | Decomposition identity (sum components) | robustness/test_robustness.py:43 | -| components/test_reward_components.py | 551 | Exit factor finiteness & plateau behavior | robustness/test_robustness.py:43+ | -| pbrs/test_pbrs.py | 1053 | Canonical vs non-canonical classification formatting | robustness/test_robustness.py:43, robustness/test_robustness.py:127 | -| pbrs/test_pbrs.py | 1415 | Non-canonical full report reason aggregation | robustness/test_robustness.py:43 | -| pbrs/test_pbrs.py | 1469 | Non-canonical mode-only reason (additives disabled) | robustness/test_robustness.py:43 | -| statistics/test_statistics.py | 292 | Mean decomposition consistency | robustness/test_robustness.py:43 | +| Non-owning test | Reference check | Owning test | +| ------------------------------------------------------------------------------ | ------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| integration/test_reward_calculation.py::test_reward_component_activation_smoke | Core component activation and decomposition | robustness/test_robustness.py::test_decomposition_integrity | +| components/test_reward_components.py::test_exit_factor_calculation | Exit factor modes and plateau behavior | robustness/test_robustness.py::test_exit_factor_comprehensive; test_plateau_continuity_at_grace_boundary | +| pbrs/test_pbrs.py::test_pbrs_canonical_near_zero_report | Canonical classification and decomposition | robustness/test_robustness.py::test_decomposition_integrity | +| pbrs/test_pbrs.py::test_pbrs_non_canonical_full_report_reason_aggregation | Non-canonical report reasons | robustness/test_robustness.py::test_decomposition_integrity | +| pbrs/test_pbrs.py::test_pbrs_non_canonical_mode_only_reason | Non-canonical exit mode without additives | robustness/test_robustness.py::test_decomposition_integrity | +| statistics/test_statistics.py::test_stats_mean_decomposition_consistency | Mean decomposition consistency | robustness/test_robustness.py::test_decomposition_integrity | ### Deprecated / Reserved IDs diff --git a/ReforceXY/reward_space_analysis/tests/api/test_api_helpers.py b/ReforceXY/reward_space_analysis/tests/api/test_api_helpers.py index f13a683..2b76b19 100644 --- a/ReforceXY/reward_space_analysis/tests/api/test_api_helpers.py +++ b/ReforceXY/reward_space_analysis/tests/api/test_api_helpers.py @@ -7,6 +7,7 @@ import tempfile import unittest from pathlib import Path from typing import Any, cast +from unittest import mock import numpy as np import pandas as pd @@ -82,6 +83,41 @@ class TestAPIAndHelpers(RewardSpaceTestBase): ) self.assertGreater(high_idle_rate_spot, low_idle_rate_spot) + def test_unmasked_sampling_probabilities_match_action_frequencies(self): + """Reported probabilities describe valid actions, not their conditional hazards.""" + draws = SCENARIOS.API_ENTRY_RATE_DRAWS + # Four standard errors using the maximum Bernoulli variance. + tolerance = 4 * math.sqrt(0.25 / draws) + cases = ( + (Positions.Neutral, False, (Actions.Long_enter,), 1), + (Positions.Neutral, True, (Actions.Long_enter, Actions.Short_enter), 1), + (Positions.Long, True, (Actions.Long_exit,), 2), + (Positions.Short, True, (Actions.Short_exit,), 2), + ) + for position, short_allowed, actions, probability_index in cases: + with self.subTest(position=position, short_allowed=short_allowed): + rng = random.Random(SEEDS.REPRODUCIBILITY) + samples = [ + _sample_action( + position, + rng, + short_allowed=short_allowed, + trade_duration=SCENARIOS.API_IDLE_DURATION_HIGH, + max_trade_duration_candles=SCENARIOS.API_MAX_IDLE_DURATION_CANDLES, + idle_duration=SCENARIOS.API_IDLE_DURATION_HIGH, + max_idle_duration_candles=SCENARIOS.API_MAX_IDLE_DURATION_CANDLES, + action_masking=False, + ) + for _ in range(draws) + ] + observed = sum(sample[0] in actions for sample in samples) / draws + self.assertAlmostEqual(samples[0][probability_index], observed, delta=tolerance) + if position == Positions.Neutral: + observed_neutral = ( + sum(sample[0] == Actions.Neutral for sample in samples) / draws + ) + self.assertAlmostEqual(samples[0][3], observed_neutral, delta=tolerance) + def test_parse_overrides(self): """Overrides accept canonical keys and reject unsupported keys.""" result = parse_overrides( @@ -191,24 +227,88 @@ class TestAPIAndHelpers(RewardSpaceTestBase): prob_upper_bound = SCENARIOS.API_PROBABILITY_UPPER_BOUND self.assertTrue(((values >= 0.0) & (values <= prob_upper_bound)).all()) - def test_simulate_samples_interprets_bool_string_params(self): - """Test simulate_samples correctly interprets string boolean params like action_masking.""" - df1 = simulate_samples_with_defaults( - self.base_params( - action_masking="true", max_trade_duration_candles=PARAMS.TRADE_DURATION_SHORT - ), - num_samples=SCENARIOS.SAMPLE_SIZE_REPORT_MINIMAL, - trading_mode="spot", + def test_unmasked_simulation_samples_invalid_actions_without_changing_position(self): + """Unmasked actions expose penalties without inventing trades or wrong-side exits.""" + params = self.base_params( + action_masking="false", max_trade_duration_candles=PARAMS.TRADE_DURATION_SHORT ) - self.assertIsInstance(df1, pd.DataFrame) - df2 = simulate_samples_with_defaults( - self.base_params( - action_masking="false", max_trade_duration_candles=PARAMS.TRADE_DURATION_SHORT - ), - num_samples=SCENARIOS.SAMPLE_SIZE_REPORT_MINIMAL, + for mode in ("spot", "futures"): + with self.subTest(mode=mode): + df = simulate_samples_with_defaults( + params, + num_samples=SCENARIOS.SAMPLE_SIZE_LARGE, + seed=SEEDS.BASE, + trading_mode=mode, + ) + invalid = df[df["is_invalid"] == 1.0] + self.assertGreater(len(invalid), 0) + self.assertLess(len(invalid), len(df)) + self.assertTrue((invalid["reward_invalid"] == params["invalid_action"]).all()) + self.assertTrue((invalid["reward_base"] == params["invalid_action"]).all()) + wrong_exit = invalid.loc[ + ( + ( + (invalid["position"] == Positions.Long.value) + & (invalid["action"] == Actions.Short_exit.value) + ) + | ( + (invalid["position"] == Positions.Short.value) + & (invalid["action"] == Actions.Long_exit.value) + ) + ) + & ~invalid["terminated"] + ] + self.assertGreater(len(wrong_exit), 0) + self.assertTrue((wrong_exit["next_position"] == wrong_exit["position"]).all()) + if mode == "spot": + forbidden_entry = invalid.loc[ + (invalid["position"] == Positions.Neutral.value) + & (invalid["action"] == Actions.Short_enter.value) + ] + self.assertGreater(len(forbidden_entry), 0) + self.assertTrue( + (forbidden_entry["next_position"] == Positions.Neutral.value).all() + ) + masked = simulate_samples_with_defaults( + self.base_params( + action_masking="true", + max_trade_duration_candles=PARAMS.TRADE_DURATION_SHORT, + ), + num_samples=SCENARIOS.SAMPLE_SIZE_LARGE, + seed=SEEDS.BASE, + trading_mode=mode, + ) + self.assertEqual(int(masked["is_invalid"].sum()), 0) + + zero_penalty = simulate_samples_with_defaults( + self.base_params(action_masking="false", invalid_action=0.0), + num_samples=SCENARIOS.SAMPLE_SIZE_LARGE, + seed=SEEDS.BASE, trading_mode="spot", ) - self.assertIsInstance(df2, pd.DataFrame) + self.assertGreater(int(zero_penalty["is_invalid"].sum()), 0) + self.assertTrue((zero_penalty["reward_invalid"] == 0.0).all()) + + def test_invalid_terminal_exit_liquidates_the_held_position(self): + """A wrong-side terminal exit keeps the held trade until its forced liquidation.""" + sampled_actions = [ + (Actions.Long_enter, 0.3, float("nan"), 0.7), + (Actions.Short_exit, float("nan"), 0.2, float("nan")), + ] + with mock.patch("reward_space_analysis._sample_action", side_effect=sampled_actions): + df = simulate_samples_with_defaults( + self.base_params(action_masking="false"), + num_samples=2, + seed=SEEDS.BASE, + trading_mode="spot", + ) + terminal = df.iloc[-1] + self.assertEqual(terminal["position"], Positions.Long.value) + self.assertEqual(terminal["action"], Actions.Short_exit.value) + self.assertEqual(terminal["is_invalid"], 1.0) + self.assertTrue(terminal["terminal_liquidation"]) + self.assertEqual(terminal["next_position"], Positions.Neutral.value) + self.assertTrue(np.isfinite(terminal["exit_pnl"])) def test_short_allowed_via_simulation(self): """Test _is_short_allowed via different trading modes.""" diff --git a/ReforceXY/reward_space_analysis/tests/cli/test_cli_params_and_csv.py b/ReforceXY/reward_space_analysis/tests/cli/test_cli_params_and_csv.py index f5a07f2..78c3494 100644 --- a/ReforceXY/reward_space_analysis/tests/cli/test_cli_params_and_csv.py +++ b/ReforceXY/reward_space_analysis/tests/cli/test_cli_params_and_csv.py @@ -167,6 +167,45 @@ class TestParamsPropagation(RewardSpaceTestBase): fi_path = out_dir / "feature_importance.csv" self.assertFalse(fi_path.exists(), "feature_importance.csv should be absent when skipped") + def test_skipped_analysis_removes_only_stale_generated_artifacts(self): + """A second run cannot expose the prior run's feature or PD conclusions.""" + out_dir = self.output_path / "reused_analysis" + args = [ + "--num_samples", + str(SCENARIOS.CLI_NUM_SAMPLES_FAST), + "--seed", + str(SEEDS.BASE), + "--rf_n_jobs", + "1", + "--perm_n_jobs", + "1", + ] + _assert_cli_success(self, _run_cli(out_dir=out_dir, args=args)) + self.assertTrue((out_dir / "feature_importance.csv").exists()) + generated_pd = list(out_dir.glob("partial_dependence_*.csv")) + self.assertGreater(len(generated_pd), 0) + user_file = out_dir / "notes.txt" + user_file.write_text("keep", encoding="utf-8") + custom_pd = out_dir / "partial_dependence_custom.csv" + custom_pd.write_text("keep", encoding="utf-8") + + _assert_cli_success( + self, + _run_cli( + out_dir=out_dir, + args=[*args, "--skip_feature_analysis", "--skip_partial_dependence"], + ), + ) + for artifact in (out_dir / "feature_importance.csv", *generated_pd): + self.assertFalse(artifact.exists(), artifact.name) + self.assertEqual(user_file.read_text(encoding="utf-8"), "keep") + self.assertEqual(custom_pd.read_text(encoding="utf-8"), "keep") + self.assertIn( + "Feature Importance - (skipped)", (out_dir / "statistical_analysis.md").read_text() + ) + self.assertTrue((out_dir / "reward_samples.csv").exists()) + self.assertTrue((out_dir / "manifest.json").exists()) + def test_manifest_records_resolved_simulation_inputs(self): """The manifest records and hashes resolved simulation inputs.""" out_dir = self.output_path / "manifest_hash" diff --git a/ReforceXY/reward_space_analysis/tests/helpers/test_utilities.py b/ReforceXY/reward_space_analysis/tests/helpers/test_utilities.py index 76cb90e..f3fefd0 100644 --- a/ReforceXY/reward_space_analysis/tests/helpers/test_utilities.py +++ b/ReforceXY/reward_space_analysis/tests/helpers/test_utilities.py @@ -170,6 +170,29 @@ class TestLoadRealEpisodes(RewardSpaceTestBase): self.assertEqual(len(loaded_data), 3) self.assertIn("pnl", loaded_data.columns) + def test_nonfinite_numeric_episodes_are_marked_missing(self): + """Keep transition multiplicity but mark infinite numeric observations missing.""" + episodes = pd.DataFrame( + { + "pnl": [0.01, float("inf"), -0.02], + "trade_duration": [1, 2, 3], + "idle_duration": [0, 0, 0], + "position": [1.0, 1.0, 1.0], + "action": [0.0, 0.0, 0.0], + "reward": [1.0, 1.0, 1.0], + "reward_exit": [0.0, float("-inf"), 0.0], + } + ) + path = Path(self.temp_dir) / "nonfinite.pkl" + self.write_pickle(episodes, path) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + loaded = load_real_episodes(path) + self.assertEqual(len(loaded), len(episodes)) + self.assertTrue(pd.isna(loaded.loc[1, "pnl"])) + self.assertTrue(pd.isna(loaded.loc[1, "reward_exit"])) + self.assertTrue(any("non-finite" in str(w.message) for w in caught)) + if __name__ == "__main__": unittest.main() diff --git a/ReforceXY/reward_space_analysis/tests/pbrs/test_pbrs.py b/ReforceXY/reward_space_analysis/tests/pbrs/test_pbrs.py index 183a306..7f2eb92 100644 --- a/ReforceXY/reward_space_analysis/tests/pbrs/test_pbrs.py +++ b/ReforceXY/reward_space_analysis/tests/pbrs/test_pbrs.py @@ -169,32 +169,32 @@ class TestSimulationParity(RewardSpaceTestBase): expected_duration = 0 self.assertEqual(row.trade_duration, expected_duration) - def test_first_retained_pnl_is_only_exit_extremum(self): - """A first-candle exit excludes the fill-time PnL from its extrema.""" + def test_synthetic_exit_reward_includes_fee_adjusted_entry_pnl(self): params = self.base_params( unrealized_pnl=False, max_trade_duration_candles=100, - entry_fee_rate=0.0, - exit_fee_rate=0.0, + entry_fee_rate=0.0015, + exit_fee_rate=0.0015, + exit_attenuation_mode="linear", + exit_plateau=False, hold_potential_enabled=False, entry_additive_enabled=False, exit_additive_enabled=False, ) actions = [ (Actions.Long_enter, 1.0, 0.0, 0.0), + (Actions.Neutral, 0.0, 0.0, 1.0), (Actions.Long_exit, 0.0, 1.0, 0.0), ] with ( patch.object(reward_space_analysis, "_sample_action", side_effect=actions), patch.object( - reward_space_analysis.random.Random, - "gauss", - side_effect=[0.02, 0.0], + reward_space_analysis.random.Random, "gauss", side_effect=[0.02, -0.01, 0.0] ), ): df = simulate_samples( params=params, - num_samples=2, + num_samples=3, seed=SEEDS.BASE, base_factor=PARAMS.BASE_FACTOR, profit_aim=PARAMS.PROFIT_AIM, @@ -205,34 +205,239 @@ class TestSimulationParity(RewardSpaceTestBase): pnl_duration_vol_scale=PARAMS.PNL_DUR_VOL_SCALE, ) - exit_row = df.iloc[1] + entry_pnl = 1.0 / ((1.0 + params["entry_fee_rate"]) * (1.0 + params["exit_fee_rate"])) - 1.0 + peak_pnl = float(df.iloc[0]["next_pnl"]) + exit_row = df.iloc[-1] pnl = float(exit_row["pnl"]) + self.assertLess(entry_pnl, 0.0) + self.assertGreater(peak_pnl, pnl) self.assertGreater(pnl, 0.0) - runtime_context = reward_space_analysis.RewardContext( - current_pnl=pnl, - trade_duration=int(exit_row["trade_duration"]), - idle_duration=0, - max_unrealized_profit=pnl, - min_unrealized_profit=pnl, - position=Positions.Long, - action=Actions.Long_exit, + efficiency = 1.0 + params["efficiency_weight"] * ( + (pnl - entry_pnl) / (peak_pnl - entry_pnl) - params["efficiency_center"] + ) + duration_ratio = float(exit_row["trade_duration"]) / params["max_trade_duration_candles"] + expected = ( + pnl + * params["base_factor"] + / (1.0 + params["exit_linear_slope"] * duration_ratio) + * efficiency ) - expected_exit = reward_space_analysis.calculate_reward( - runtime_context, - params, - PARAMS.BASE_FACTOR, - PARAMS.PROFIT_AIM, - PARAMS.RISK_REWARD_RATIO, - short_allowed=True, - action_masking=True, - ).exit_component self.assertAlmostEqualFloat( float(exit_row["reward_exit"]), - expected_exit, + expected, tolerance=TOLERANCE.IDENTITY_RELAXED, rtol=TOLERANCE.RELATIVE, ) + def test_synthetic_fee_loss_extrema_match_retained_pnl(self): + for direction in ("long", "short"): + for transformed in (False, True): + with self.subTest(direction=direction, transformed=transformed): + params = self.base_params( + entry_fee_rate=0.1, + exit_fee_rate=0.1, + unrealized_pnl=transformed, + max_trade_duration_candles=1, + win_reward_factor=0.0, + exit_plateau=False, + hold_potential_enabled=False, + exit_linear_slope=0.0, + entry_additive_enabled=False, + exit_additive_enabled=False, + ) + enter = Actions.Long_enter if direction == "long" else Actions.Short_enter + exit_action = Actions.Long_exit if direction == "long" else Actions.Short_exit + # Cancel the simulator's directional drift to keep both marks flat. + innovation = -0.001 if direction == "long" else 0.001 + with ( + patch.object( + reward_space_analysis, + "_sample_action", + side_effect=[(enter, 1.0, 0.0, 0.0), (exit_action, 0.0, 1.0, 0.0)], + ), + patch.object( + reward_space_analysis.random.Random, + "gauss", + side_effect=[innovation, 0.0], + ), + ): + samples = simulate_samples( + num_samples=2, + seed=SEEDS.BASE, + params=params, + base_factor=PARAMS.BASE_FACTOR, + profit_aim=PARAMS.PROFIT_AIM, + risk_reward_ratio=PARAMS.RISK_REWARD_RATIO, + max_duration_ratio=2.0, + trading_mode="futures", + pnl_base_std=PARAMS.PNL_STD, + pnl_duration_vol_scale=PARAMS.PNL_DUR_VOL_SCALE, + ) + + fees = (1.0 + params["entry_fee_rate"]) * (1.0 + params["exit_fee_rate"]) + entry_pnl = 1.0 / fees - 1.0 if direction == "long" else 1.0 - fees + expected_pnl = ( + entry_pnl * math.tanh(params["pnl_amplification_sensitivity"]) + if transformed + else entry_pnl + ) + self.assertLess(expected_pnl, -0.15) + first, exit_row = samples.iloc[0], samples.iloc[1] + self.assertAlmostEqual(float(first["next_pnl"]), expected_pnl, places=9) + self.assertAlmostEqual(float(exit_row["exit_pnl"]), expected_pnl, places=9) + efficiency = ( + 1.0 + params["efficiency_weight"] * (params["efficiency_center"] - 1.0) + if transformed + else 1.0 + ) + self.assertAlmostEqual( + float(exit_row["reward_exit"]), + expected_pnl * params["base_factor"] * efficiency, + places=8, + ) + + def test_synthetic_high_fee_winner_retains_profitable_mark(self): + """Keep favorable retained PnL and exit reward positive with high fees. + + **Invariant:** pbrs-synthetic-profitable-mark-133 + """ + for direction, innovation in (("long", 0.299), ("short", -0.299)): + for flat_first in (False, True): + with self.subTest(direction=direction, flat_first=flat_first): + params = self.base_params( + unrealized_pnl=True, + entry_fee_rate=0.1, + exit_fee_rate=0.1, + max_trade_duration_candles=1, + ) + enter = Actions.Long_enter if direction == "long" else Actions.Short_enter + exit_action = Actions.Long_exit if direction == "long" else Actions.Short_exit + actions = [(enter, 1.0, 0.0, 0.0)] + # The directional drift completes a 30% favorable market move. + innovations = [innovation] + if flat_first: + actions.append((Actions.Neutral, 0.0, 0.0, 1.0)) + innovations = [-0.001 if direction == "long" else 0.001, innovation] + actions.append((exit_action, 0.0, 1.0, 0.0)) + innovations.append(0.0) + with ( + patch.object(reward_space_analysis, "_sample_action", side_effect=actions), + patch.object( + reward_space_analysis.random.Random, + "gauss", + side_effect=innovations, + ), + ): + samples = simulate_samples( + num_samples=len(actions), + seed=SEEDS.BASE, + params=params, + base_factor=PARAMS.BASE_FACTOR, + profit_aim=PARAMS.PROFIT_AIM, + risk_reward_ratio=PARAMS.RISK_REWARD_RATIO, + max_duration_ratio=2.0, + trading_mode="futures", + pnl_base_std=PARAMS.PNL_STD, + pnl_duration_vol_scale=PARAMS.PNL_DUR_VOL_SCALE, + ) + if flat_first: + self.assertLess(float(samples.iloc[0]["next_pnl"]), 0.0) + self.assertGreater(float(samples.iloc[-2]["next_pnl"]), 0.0) + self.assertGreater(float(samples.iloc[-1]["exit_pnl"]), 0.0) + self.assertGreater(float(samples.iloc[-1]["reward_exit"]), 0.0) + + def test_synthetic_high_fee_short_boundary_rejects_real_excess(self): + """Accept rounding at the fee bound but reject a material loss beyond it. + + **Invariant:** pbrs-synthetic-fee-boundary-134 + """ + params = self.base_params( + entry_fee_rate=0.1, + exit_fee_rate=0.1, + max_trade_duration_candles=1, + ) + with ( + patch.object( + reward_space_analysis, + "_sample_action", + side_effect=[ + (Actions.Neutral, 0.0, 0.0, 1.0), + (Actions.Short_enter, 1.0, 0.0, 0.0), + (Actions.Short_exit, 0.0, 1.0, 0.0), + ], + ), + patch.object( + reward_space_analysis.random.Random, + "gauss", + # Neutral move sets entry price to 1.03; next move cancels short drift. + side_effect=[0.03, 0.001, 0.0], + ), + ): + samples = simulate_samples( + num_samples=3, + seed=SEEDS.BASE, + params=params, + base_factor=PARAMS.BASE_FACTOR, + profit_aim=PARAMS.PROFIT_AIM, + risk_reward_ratio=PARAMS.RISK_REWARD_RATIO, + max_duration_ratio=2.0, + trading_mode="futures", + pnl_base_std=PARAMS.PNL_STD, + pnl_duration_vol_scale=PARAMS.PNL_DUR_VOL_SCALE, + ) + fee_factor = (1.0 + params["entry_fee_rate"]) * (1.0 + params["exit_fee_rate"]) + self.assertAlmostEqual(float(samples.iloc[-1]["pnl"]), 1.0 - fee_factor, places=12) + beyond_bound = samples.copy() + beyond_bound.loc[beyond_bound.index[-1], "pnl"] = 1.0 - fee_factor - 0.001 + with self.assertRaisesRegex(AssertionError, "extreme PnL"): + reward_space_analysis._validate_simulation_invariants(beyond_bound, params) + + def test_unrealized_pnl_retains_sampled_market_path_after_candidate_cap(self): + """Keep distinct market paths after their first retained PnL is capped. + + **Invariant:** pbrs-synthetic-latent-price-135 + """ + params = self.base_params(unrealized_pnl=True, max_trade_duration_candles=1) + actions = [ + (Actions.Long_enter, 1.0, 0.0, 0.0), + (Actions.Neutral, 0.0, 0.0, 1.0), + (Actions.Long_exit, 0.0, 1.0, 0.0), + ] + + def sample(first_return: float) -> pd.DataFrame: + with ( + patch.object(reward_space_analysis, "_sample_action", side_effect=actions), + patch.object( + reward_space_analysis.random.Random, + "gauss", + # Both first candidates clip at +0.15; the following move is -30%. + side_effect=[first_return, -0.301, 0.0], + ), + ): + return simulate_samples( + num_samples=len(actions), + seed=SEEDS.BASE, + params=params, + base_factor=PARAMS.BASE_FACTOR, + profit_aim=PARAMS.PROFIT_AIM, + risk_reward_ratio=PARAMS.RISK_REWARD_RATIO, + max_duration_ratio=2.0, + trading_mode="futures", + pnl_base_std=PARAMS.PNL_STD, + pnl_duration_vol_scale=PARAMS.PNL_DUR_VOL_SCALE, + ) + + lower_price = sample(0.599) + higher_price = sample(0.949) + self.assertAlmostEqual( + float(lower_price.iloc[0]["next_pnl"]), float(higher_price.iloc[0]["next_pnl"]) + ) + self.assertGreater(float(lower_price.iloc[1]["next_pnl"]), 0.0) + self.assertGreater( + float(higher_price.iloc[1]["next_pnl"]), + float(lower_price.iloc[1]["next_pnl"]), + ) + def test_unrealized_pnl_uses_each_sampled_market_move(self): """Later Gaussian innovations affect later retained PnL without becoming extrema.""" params = self.base_params( @@ -334,8 +539,8 @@ class TestSimulationParity(RewardSpaceTestBase): ) self.assertTrue(replay["next_pnl"].equals(df["next_pnl"])) - def test_unrealized_pnl_exit_rewards_use_only_retained_extrema(self): - """Exit rewards use extrema reconstructed from the retained PnL trajectory.""" + def test_unrealized_pnl_exit_rewards_include_fill_and_retained_extrema(self): + """The fee-adjusted fill and retained prices determine exit efficiency.""" params = self.base_params( unrealized_pnl=True, max_trade_duration_candles=100, @@ -378,8 +583,11 @@ class TestSimulationParity(RewardSpaceTestBase): Actions.Long_enter, Actions.Short_enter, ): - max_unrealized = -np.inf - min_unrealized = np.inf + fee_factor = (1.0 + params["entry_fee_rate"]) * (1.0 + params["exit_fee_rate"]) + entry_pnl = ( + 1.0 / fee_factor - 1.0 if action == Actions.Long_enter else 1.0 - fee_factor + ) + max_unrealized = min_unrealized = entry_pnl if position in (Positions.Long, Positions.Short) and action in ( Actions.Long_exit, diff --git a/ReforceXY/reward_space_analysis/tests/statistics/test_statistics.py b/ReforceXY/reward_space_analysis/tests/statistics/test_statistics.py index e32f445..b5bdc47 100644 --- a/ReforceXY/reward_space_analysis/tests/statistics/test_statistics.py +++ b/ReforceXY/reward_space_analysis/tests/statistics/test_statistics.py @@ -715,6 +715,63 @@ class TestStatistics(RewardSpaceTestBase): strict_diagnostics=strict, ) + def test_distribution_shift_uses_finite_observations(self): + """One infinite PnL cannot erase a distribution with enough finite samples.""" + synthetic = pd.DataFrame( + { + "pnl": np.linspace(-0.1, 0.1, 20), + "trade_duration": np.arange(20), + "idle_duration": np.arange(20), + } + ) + real = synthetic.copy() + real.loc[0, "pnl"] = np.inf + observed = compute_distribution_shift_metrics(synthetic, real) + finite_only = real.copy() + finite_only.loc[0, "pnl"] = np.nan + expected = compute_distribution_shift_metrics(synthetic, finite_only) + for key in ("pnl_kl_divergence", "pnl_js_distance", "pnl_wasserstein", "pnl_ks_statistic"): + self.assertIn(key, observed) + self.assertAlmostEqual(observed[key], expected[key]) + + def test_pnl_rank_biserial_direction_matches_named_first_group(self): + """A larger reward for pnl+ has a positive effect; reversing groups reverses it.""" + df = pd.DataFrame( + { + "pnl": [1.0] * 30 + [-1.0] * 30, + "reward": [10.0] * 30 + [0.0] * 30, + "reward_idle": [0.0] * 60, + "position": [1.0] * 60, + } + ) + for positive_reward, negative_reward, expected_u, expected_effect in ( + (10.0, 0.0, 900.0, 1.0), + (0.0, 10.0, 0.0, -1.0), + ): + with self.subTest(positive_reward=positive_reward): + df.loc[:29, "reward"] = positive_reward + df.loc[30:, "reward"] = negative_reward + result = statistical_hypothesis_tests(df, independent_observations=True)[ + "pnl_sign_reward_difference" + ] + self.assertEqual(result["statistic"], expected_u) + self.assertEqual(result["effect_size_rank_biserial"], expected_effect) + + def test_bootstrap_rejects_nonpositive_resample_count(self): + """Reject an absent bootstrap even when constant data take the fast path.""" + for rewards in (np.arange(10, dtype=float), np.ones(10)): + for count in (0, -1): + with ( + self.subTest(constant=bool(rewards.min() == rewards.max()), count=count), + self.assertRaisesRegex(ValueError, "n_bootstrap"), + ): + bootstrap_confidence_intervals( + pd.DataFrame({"reward": rewards}), + ["reward"], + n_bootstrap=count, + independent_observations=True, + ) + if __name__ == "__main__": unittest.main() diff --git a/ReforceXY/tests/test_portfolio_returns.py b/ReforceXY/tests/test_portfolio_returns.py index b545b73..1fc5f21 100644 --- a/ReforceXY/tests/test_portfolio_returns.py +++ b/ReforceXY/tests/test_portfolio_returns.py @@ -6,7 +6,7 @@ import unittest import numpy as np import pandas as pd -from ReforceXY.user_data.freqaimodels.ReforceXY import Actions, MyRLEnv +from ReforceXY.user_data.freqaimodels.ReforceXY import Actions, MyRLEnv, ReforceXY class PortfolioReturnsTest(unittest.TestCase): @@ -46,6 +46,46 @@ class PortfolioReturnsTest(unittest.TestCase): self.assertEqual(info["most_recent_profit"], 0.0) self.assertAlmostEqual(np.expm1(env.portfolio_log_returns.sum()), env._total_profit - 1) + def test_exit_reward_includes_fee_adjusted_entry_extremum(self): + for short, mark in ((False, 110.0), (True, 90.0)): + with self.subTest(short=short): + env = self.make_env([100.0, 100.0, mark, mark, mark]) + entry = Actions.Short_enter if short else Actions.Long_enter + exit_action = Actions.Short_exit if short else Actions.Long_exit + fee_factor = (1.0 + env.fee) ** 2 + entry_pnl = 1.0 - fee_factor if short else 1.0 / fee_factor - 1.0 + exit_pnl = ( + 1.0 - mark / 100.0 * fee_factor if short else mark / 100.0 / fee_factor - 1.0 + ) + + env.step(entry.value) + self.assertAlmostEqual(env.get_min_unrealized_profit(), entry_pnl) + self.assertAlmostEqual(env.get_max_unrealized_profit(), exit_pnl) + _, reward, _, _, _ = env.step(exit_action.value) + + duration = 1.0 / env.max_trade_duration_candles + if ReforceXY.DEFAULT_EXIT_PLATEAU: + duration = max(0.0, duration - ReforceXY.DEFAULT_EXIT_PLATEAU_GRACE) + attenuation = 1.0 / (1.0 + ReforceXY.DEFAULT_EXIT_LINEAR_SLOPE * duration) + target_coefficient = 1.0 + if exit_pnl > env._pnl_target: + target_coefficient += ReforceXY.DEFAULT_WIN_REWARD_FACTOR * math.tanh( + ReforceXY.DEFAULT_PNL_AMPLIFICATION_SENSITIVITY + * (exit_pnl / env._pnl_target - 1.0) + ) + efficiency = 1.0 + ReforceXY.DEFAULT_EFFICIENCY_WEIGHT * ( + 1.0 - ReforceXY.DEFAULT_EFFICIENCY_CENTER + ) + self.assertAlmostEqual( + reward, + exit_pnl + * ReforceXY.DEFAULT_BASE_FACTOR + * attenuation + * target_coefficient + * efficiency, + places=9, + ) + def test_transitions_match_equity_for_both_staking_modes_and_directions(self): prices = [100.0, 100.0, 110.0, 105.0, 115.0, 120.0, 118.0, 117.0, 117.0] for short in (False, True): @@ -138,7 +178,6 @@ class PortfolioReturnsTest(unittest.TestCase): env.trade_history[-1]["type"], "short_exit" if short else "long_exit", ) - self.assertEqual(env.trade_history[-1]["tick"], info["execution_tick"]) self.assertEqual(env.trade_history[-1]["price"], terminal_price) self.assertAlmostEqual(env.trade_history[-1]["profit"], expected_pnl) self.assertAlmostEqual( @@ -212,7 +251,7 @@ class PortfolioReturnsTest(unittest.TestCase): else: pd.testing.assert_frame_equal(history, baseline) - def test_terminal_entry_preserves_two_events_and_plot_markers(self): + def test_terminal_fill_events_and_plot_match_their_price_candles(self): env = self.make_env([100.0, 100.0, 90.0]) _, _, terminated, truncated, info = env.step(Actions.Long_enter.value) expected_pnl = 90.0 / 100.0 / (1.0 + env.fee) ** 2 - 1.0 @@ -225,26 +264,29 @@ class PortfolioReturnsTest(unittest.TestCase): self.assertEqual(history.iloc[0]["execution_tick"], info["execution_tick"]) self.assertEqual(history.iloc[0]["open"], 90.0) - self.assertEqual(len(env.trade_history), 2) - self.assertEqual( - [(event["tick"], event["type"]) for event in env.trade_history], - [(info["execution_tick"], "long_enter"), (info["execution_tick"], "long_exit")], - ) - self.assertEqual( - [event["price"] for event in env.trade_history], - [100.0, 90.0], - ) - self.assertEqual(env.trade_history[0]["profit"], 0.0) - self.assertAlmostEqual(env.trade_history[1]["profit"], expected_pnl) + events = env.trade_history + self.assertEqual([event["type"] for event in events], ["long_enter", "long_exit"]) + for event in events: + self.assertAlmostEqual(event["price"], env.prices.iloc[event["tick"]]["open"]) + self.assertEqual(events[0]["profit"], 0.0) + self.assertAlmostEqual(events[1]["profit"], expected_pnl) figure = env.get_env_plot() - marker_lines = [ + curve = figure.axes[0].lines[0] + markers = [ line for line in figure.axes[0].lines if line.get_linestyle() == "None" and len(line.get_xdata()) == 1 ] - self.assertEqual([line.get_marker() for line in marker_lines], ["^", "."]) - self.assertEqual([line.get_xdata()[0] for line in marker_lines], [1, 1]) + self.assertEqual([line.get_marker() for line in markers], ["^", "."]) + self.assertEqual( + [(line.get_xdata()[0], line.get_ydata()[0]) for line in markers], + [(event["tick"], event["price"]) for event in events], + ) + self.assertEqual( + (markers[-1].get_xdata()[0], markers[-1].get_ydata()[0]), + (curve.get_xdata()[-1], curve.get_ydata()[-1]), + ) def test_nonpositive_equity_is_not_reported_as_zero_return(self): env = self.make_env([100.0, 100.0, 100.0, 300.0, 300.0], fee=0.0) diff --git a/ReforceXY/tests/test_review_contracts.py b/ReforceXY/tests/test_review_contracts.py index 09c00b8..59ad196 100644 --- a/ReforceXY/tests/test_review_contracts.py +++ b/ReforceXY/tests/test_review_contracts.py @@ -24,6 +24,7 @@ from ReforceXY.user_data.freqaimodels.ReforceXY import ( convert_optuna_params_to_model_params, deepmerge, ) +from ReforceXY.user_data.strategies.RLAgentStrategy import RLAgentStrategy class RecordingPolicy: @@ -101,6 +102,64 @@ class ReviewContractsTest(unittest.TestCase): self.addCleanup(model.close_envs) return model + def test_strategy_leverage_respects_pair_bounds_and_invalid_config(self): + """A strategy callback always returns a finite leverage inside pair limits.""" + strategy = RLAgentStrategy.__new__(RLAgentStrategy) + arguments = { + "pair": "BTC/USDT", + "current_time": dt(2026, 1, 1, tzinfo=timezone.utc), + "current_rate": 100.0, + "proposed_leverage": 2.0, + "max_leverage": 5.0, + "entry_tag": None, + "side": "long", + } + for configured, expected in ( + (None, 2.0), + (0.5, 1.0), + (10.0, 5.0), + (3.0, 3.0), + (float("nan"), 2.0), + (float("inf"), 2.0), + (10**500, 2.0), + (-5.0, 1.0), + ("invalid", 2.0), + (True, 2.0), + ): + with self.subTest(configured=configured): + strategy.config = {} if configured is None else {"leverage": configured} + self.assertEqual(strategy.leverage(**arguments), expected) + + def test_strategy_leverage_warnings_follow_invalid_config_transitions(self): + """Warn once per invalid setting, including values below the leverage floor.""" + strategy = RLAgentStrategy.__new__(RLAgentStrategy) + arguments = { + "pair": "BTC/USDT", + "current_time": dt(2026, 1, 1, tzinfo=timezone.utc), + "current_rate": 100.0, + "proposed_leverage": 2.0, + "max_leverage": 5.0, + "entry_tag": None, + "side": "long", + } + strategy.config = {"leverage": float("nan")} + with self.assertLogs(RLAgentStrategy.__module__, level="WARNING") as logs: + for _ in range(3): + self.assertEqual(strategy.leverage(**arguments), 2.0) + self.assertEqual(len(logs.records), 1) + strategy.config["leverage"] = float("inf") + self.assertEqual(strategy.leverage(**arguments), 2.0) + self.assertEqual(len(logs.records), 2) + strategy.config["leverage"] = 0.5 + for _ in range(2): + self.assertEqual(strategy.leverage(**arguments), 1.0) + self.assertEqual(len(logs.records), 3) + strategy.config["leverage"] = 2.5 + self.assertEqual(strategy.leverage(**arguments), 2.5) + strategy.config["leverage"] = float("nan") + self.assertEqual(strategy.leverage(**arguments), 2.0) + self.assertEqual(len(logs.records), 4) + def test_training_preserves_raw_prices_and_returns_best_checkpoint(self): for drop in (False, True): with self.subTest(drop_ohlc_from_features=drop), tempfile.TemporaryDirectory() as temp: @@ -667,16 +726,11 @@ class ReviewContractsTest(unittest.TestCase): ) self.assertAlmostEqual(entry_reward, env._potential_gamma * expected_potential) - _, exit_reward, exit_done, exit_truncated, _ = env.step(2) + _, _, exit_done, exit_truncated, _ = env.step(2) self.assertFalse(exit_done) self.assertFalse(exit_truncated) self.assertEqual(env.trade_history[-1]["tick"], 3) self.assertEqual(env.trade_history[-1]["price"], 110.0) - pnl_coefficient = 1.0 + 2.0 * math.tanh(2.0 * (observed_pnl / env._pnl_target - 1.0)) - # The entry transition retains one PnL observation, so max equals min - # and the independent efficiency formula remains at its neutral value 1.0. - expected_base_exit = observed_pnl * ReforceXY.DEFAULT_BASE_FACTOR * pnl_coefficient - self.assertAlmostEqual(exit_reward, expected_base_exit - expected_potential, places=5) env.step(1) _, _, done, truncated, info = env.step(0) diff --git a/ReforceXY/tests/test_training_observations.py b/ReforceXY/tests/test_training_observations.py index 6fa931a..df0e032 100644 --- a/ReforceXY/tests/test_training_observations.py +++ b/ReforceXY/tests/test_training_observations.py @@ -8,7 +8,9 @@ from unittest import mock import numpy as np import pandas as pd +from freqtrade.enums import RunMode from freqtrade.exceptions import DependencyException +from freqtrade.exchange import timeframe_to_seconds from freqtrade.freqai.data_drawer import FreqaiDataDrawer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from optuna import TrialPruned, create_study @@ -120,6 +122,116 @@ class TrainingObservationsTest(unittest.TestCase): ) self.assertTrue(np.isfinite(score)) + def test_backtest_rejects_future_archive_but_continues_from_earlier_saved_window(self): + with tempfile.TemporaryDirectory() as temp: + config = model_config(temp) + info = config["freqai"] + info["continual_learning"] = True + info["rl_config"]["model_type"] = "DQN" + info["model_training_parameters"] = { + "learning_starts": 0, + "buffer_size": 128, + "batch_size": 8, + "train_freq": 4, + "gradient_steps": 1, + "device": "cpu", + "policy_kwargs": {"net_arch": [8]}, + } + pair = "BTC/USDT" + + def frame(day, feature_offset): + values = np.arange(64) + data = pd.DataFrame( + { + "date": pd.date_range(day, periods=64, freq="5min", tz="UTC"), + "%-feature": np.sin(values) + feature_offset, + "&-action": np.zeros(64), + } + ) + for column in ("open", "high", "low", "close"): + data[f"%-raw_{column}"] = 100.0 + values * 0.1 + return data + + def kitchen(settings, data, *, live): + dk = FreqaiDataKitchen(settings, live=live, pair=pair) + timestamp = int( + ( + data["date"].iloc[-1] + + pd.Timedelta(seconds=timeframe_to_seconds(settings["timeframe"])) + ).timestamp() + ) + dk.set_paths(pair, timestamp) + dk.set_new_model_names(pair, timestamp) + dk.data_path.mkdir(parents=True, exist_ok=True) + dk.label_list = ["&-action"] + dk.training_features_list = [column for column in data if column.startswith("%")] + return dk, timestamp + + source = ReforceXY(config=config) + source.live = True + source.can_short = False + self.addCleanup(source.close_envs) + future = frame("2026-02-01", 1000.0) + future_dk, future_ts = kitchen(config, future, live=True) + deployed = source.train(future, pair, future_dk) + source.dd.get_pair_dict_info(pair) + source.dd.pair_dict[pair]["trained_timestamp"] = future_ts + source.dd.save_data(deployed, pair, future_dk) + + backtest_config = dict(config) + backtest_config["runmode"] = RunMode.BACKTEST + backtest_config["timerange"] = "20260101-20260105" + backtest_config["config_files"] = [ + "/workspace/ReforceXY/user_data/config-template.json" + ] + backtest = ReforceXY(config=backtest_config) + backtest.live = False + backtest.can_short = False + self.addCleanup(backtest.close_envs) + saved_timestamp = 0 + for day, offset, save_model in ( + ("2026-01-01", 0.0, False), + ("2026-01-02", 10.0, True), + ("2026-01-03", 20.0, False), + ): + training_frame = frame(day, offset) + dk, timestamp = kitchen(backtest_config, training_frame, live=False) + self.assertLess(timestamp, future_ts) + self.assertFalse(backtest.model_exists(dk)) + trained = backtest.train(training_frame, pair, dk) + transformed = dk.data_dictionary["train_features"]["%-feature"] + if offset < 20.0: + self.assertAlmostEqual(transformed.min(), -1.0) + self.assertAlmostEqual(transformed.max(), 1.0) + else: + self.assertGreater(transformed.min(), 2.0) + backtest.dd.pair_dict[pair]["trained_timestamp"] = timestamp + if save_model: + backtest.dd.save_data(trained, pair, dk) + saved_timestamp = timestamp + else: + backtest.dd.save_metadata(dk) + + self.assertGreater(saved_timestamp, 0) + seconds_config = dict(backtest_config) + seconds_config["timeframe"] = "10s" + seconds = ReforceXY(config=seconds_config) + seconds.live = False + seconds.can_short = False + self.addCleanup(seconds.close_envs) + seconds.training_timerange = SimpleNamespace(stopts=saved_timestamp + 10) + boundary = frame("2026-01-02", 20.0) + boundary["date"] = pd.date_range( + end=pd.Timestamp(saved_timestamp, unit="s", tz="UTC"), + periods=len(boundary), + freq="10s", + ) + boundary_dk, boundary_timestamp = kitchen(seconds_config, boundary, live=False) + self.assertEqual(boundary_timestamp, saved_timestamp + 10) + seconds.train(boundary, pair, boundary_dk) + transformed = boundary_dk.data_dictionary["train_features"]["%-feature"] + self.assertGreater(transformed.min(), 2.0) + def test_provenance_does_not_change_other_drawers(self): with tempfile.TemporaryDirectory() as temp: config = model_config(temp) diff --git a/ReforceXY/user_data/freqaimodels/ReforceXY.py b/ReforceXY/user_data/freqaimodels/ReforceXY.py index f12d205..200135a 100644 --- a/ReforceXY/user_data/freqaimodels/ReforceXY.py +++ b/ReforceXY/user_data/freqaimodels/ReforceXY.py @@ -36,6 +36,7 @@ import rapidjson import torch as th from datasieve.pipeline import Pipeline from freqtrade.exceptions import DependencyException +from freqtrade.exchange import timeframe_to_seconds from freqtrade.freqai.data_drawer import ( FEATURE_PIPELINE, METADATA, @@ -1281,7 +1282,7 @@ class ReforceXY(BaseReinforcementLearningModel): return callbacks def _resolve_deployment_state( - self, dk: FreqaiDataKitchen, pair: str + self, dk: FreqaiDataKitchen, pair: str, *, as_of_ts: int | None = None ) -> tuple[Any, Pipeline] | None: """Restore independent training copies of the deployed policy and feature pipeline.""" if not self.continual_learning: @@ -1291,6 +1292,19 @@ class ReforceXY(BaseReinforcementLearningModel): previous = self.dd.pair_dict.get(pair, {}) if model is None and not previous.get("model_filename"): return None + if not self.live: + # The drawer timestamp may advance without saving a new policy. + # Bound the actual artifact before loading policy, pipeline or replay. + name, separator, timestamp = previous.get("model_filename", "").rpartition("_") + if ( + as_of_ts is None + or not previous.get("trained_timestamp") + or not separator + or name != f"cb_{pair.split('/')[0].lower()}" + or not timestamp.isdecimal() + or int(timestamp) >= as_of_ts + ): + return None try: cached = self.dd.meta_data_dictionary.get(pair, {}) metadata = cached.get(METADATA) @@ -1432,7 +1446,14 @@ class ReforceXY(BaseReinforcementLearningModel): dk.fit_labels() # Capture prices once, before normalization and optional raw-OHLC removal. prices_train, prices_test = self.build_ohlc_price_dataframes(raw_data, pair, dk) - deployment_state = self._resolve_deployment_state(dk, pair) + as_of_ts = None + if self.continual_learning and not self.live: + latest_date = pd.to_datetime(unfiltered_df["date"], utc=True).max() + as_of_ts = int(latest_date.timestamp()) + timeframe_to_seconds(self.config["timeframe"]) + training_timerange = getattr(self, "training_timerange", None) + if training_timerange is not None: + as_of_ts = min(as_of_ts, int(training_timerange.stopts)) + deployment_state = self._resolve_deployment_state(dk, pair, as_of_ts=as_of_ts) dd = self._apply_training_pipeline( raw_data, dk, deployment_state=None if self.hyperopt else deployment_state ) @@ -4114,6 +4135,9 @@ class MyRLEnv(Base5ActionRLEnv): reward = self.calculate_reward(action) trade_type = self.execute_trade(action) entry_pnl = self.get_unrealized_profit() if previous_position == Positions.Neutral else 0.0 + if trade_type is not None and self._position in (Positions.Long, Positions.Short): + self._update_max_unrealized_profit(entry_pnl) + self._update_min_unrealized_profit(entry_pnl) if trade_type is not None: self.append_trade_history( trade_type, self.current_price(), pre_pnl, execution_tick=execution_tick @@ -4189,7 +4213,7 @@ class MyRLEnv(Base5ActionRLEnv): f"{closed_position.name}_exit", self.current_price(), terminal_pnl, - execution_tick=execution_tick, + execution_tick=self._current_tick, ) if terminated: reward = self._apply_terminal_pbrs_correction(reward) diff --git a/ReforceXY/user_data/strategies/RLAgentStrategy.py b/ReforceXY/user_data/strategies/RLAgentStrategy.py index fe1212a..759e05c 100644 --- a/ReforceXY/user_data/strategies/RLAgentStrategy.py +++ b/ReforceXY/user_data/strategies/RLAgentStrategy.py @@ -58,6 +58,7 @@ class RLAgentStrategy(IStrategy): _ACTION_EXIT_LONG: Final[int] = 2 _ACTION_ENTER_SHORT: Final[int] = 3 _ACTION_EXIT_SHORT: Final[int] = 4 + _leverage_warning_state: tuple[str, str] | None = None @property def can_short(self) -> bool: @@ -188,7 +189,31 @@ class RLAgentStrategy(IStrategy): :param side: 'long' or 'short' - indicating the direction of the proposed trade :return: A leverage amount, which will be between 1.0 and max_leverage. """ - return min(self.config.get("leverage", proposed_leverage), max_leverage) + configured = self.config.get("leverage") + warning_kind: str | None = None + if configured is None: + requested = proposed_leverage + else: + try: + requested = float(configured) + except (TypeError, ValueError, OverflowError): + requested = float("nan") + if isinstance(configured, bool) or not np.isfinite(requested): + warning_kind = "invalid" + requested = proposed_leverage + elif requested < 1.0: + warning_kind = "below_minimum" + + warning_state = (warning_kind, repr(configured)) if warning_kind else None + if warning_state is not None and warning_state != self._leverage_warning_state: + if warning_kind == "invalid": + logger.warning("Invalid leverage value %r; using proposed leverage", configured) + else: + logger.warning( + "Invalid leverage value %r; must be >= 1.0, clamping to 1.0", configured + ) + self._leverage_warning_state = warning_state + return float(max(1.0, min(requested, max_leverage))) def is_short_allowed(self) -> bool: trading_mode = self.config.get("trading_mode") diff --git a/quickadapter/.basedpyright/diagnostics.json b/quickadapter/.basedpyright/diagnostics.json index 0684d8a..fe96aa3 100644 --- a/quickadapter/.basedpyright/diagnostics.json +++ b/quickadapter/.basedpyright/diagnostics.json @@ -243,503 +243,503 @@ }, { "endCharacter": 87, - "endLine": 2602, + "endLine": 2621, "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": 2602 + "startLine": 2621 }, { "endCharacter": 68, - "endLine": 2603, + "endLine": 2622, "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": 2603 + "startLine": 2622 }, { "endCharacter": 63, - "endLine": 2614, + "endLine": 2633, "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": 2614 + "startLine": 2633 }, { "endCharacter": 47, - "endLine": 2616, + "endLine": 2635, "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": 2616 + "startLine": 2635 }, { "endCharacter": 47, - "endLine": 2616, + "endLine": 2635, "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": 2616 + "startLine": 2635 }, { "endCharacter": 43, - "endLine": 2617, + "endLine": 2636, "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": 2617 + "startLine": 2636 }, { "endCharacter": 43, - "endLine": 2617, + "endLine": 2636, "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": 2617 + "startLine": 2636 }, { "endCharacter": 31, - "endLine": 2638, + "endLine": 2657, "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": 2638 + "startLine": 2657 }, { "endCharacter": 31, - "endLine": 2638, + "endLine": 2657, "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": 2638 + "startLine": 2657 }, { "endCharacter": 60, - "endLine": 2638, + "endLine": 2657, "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": 2638 + "startLine": 2657 }, { "endCharacter": 60, - "endLine": 2638, + "endLine": 2657, "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": 2638 + "startLine": 2657 }, { "endCharacter": 73, - "endLine": 2650, + "endLine": 2669, "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": 2650 + "startLine": 2669 }, { "endCharacter": 73, - "endLine": 2650, + "endLine": 2669, "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": 2650 + "startLine": 2669 }, { "endCharacter": 83, - "endLine": 2651, + "endLine": 2670, "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": 2651 + "startLine": 2670 }, { "endCharacter": 83, - "endLine": 2651, + "endLine": 2670, "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": 2651 + "startLine": 2670 }, { "endCharacter": 46, - "endLine": 2886, + "endLine": 2905, "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": 2886 + "startLine": 2905 }, { "endCharacter": 46, - "endLine": 2886, + "endLine": 2905, "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": 2886 + "startLine": 2905 }, { "endCharacter": 46, - "endLine": 2886, + "endLine": 2905, "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": 2886 + "startLine": 2905 }, { "endCharacter": 46, - "endLine": 2886, + "endLine": 2905, "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": 2886 + "startLine": 2905 }, { "endCharacter": 46, - "endLine": 2886, + "endLine": 2905, "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": 2886 + "startLine": 2905 }, { "endCharacter": 46, - "endLine": 2886, + "endLine": 2905, "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": 2886 + "startLine": 2905 }, { "endCharacter": 46, - "endLine": 2886, + "endLine": 2905, "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": 2886 + "startLine": 2905 }, { "endCharacter": 41, - "endLine": 2893, + "endLine": 2912, "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": 2893 + "startLine": 2912 }, { "endCharacter": 39, - "endLine": 2942, + "endLine": 2961, "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": 2942 + "startLine": 2961 }, { "endCharacter": 19, - "endLine": 3052, + "endLine": 3071, "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": 3052 + "startLine": 3071 }, { "endCharacter": 21, - "endLine": 3164, + "endLine": 3183, "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": 3164 + "startLine": 3183 }, { "endCharacter": 21, - "endLine": 3165, + "endLine": 3184, "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": 3165 + "startLine": 3184 }, { "endCharacter": 33, - "endLine": 3166, + "endLine": 3185, "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": 3166 + "startLine": 3185 }, { "endCharacter": 32, - "endLine": 3167, + "endLine": 3186, "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": 3167 + "startLine": 3186 }, { "endCharacter": 32, - "endLine": 3168, + "endLine": 3187, "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": 3168 + "startLine": 3187 }, { "endCharacter": 38, - "endLine": 3169, + "endLine": 3188, "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": 3169 + "startLine": 3188 }, { "endCharacter": 24, - "endLine": 3190, + "endLine": 3209, "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": 3190 + "startLine": 3209 }, { "endCharacter": 24, - "endLine": 3191, + "endLine": 3210, "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": 3191 + "startLine": 3210 }, { "endCharacter": 30, - "endLine": 3192, + "endLine": 3211, "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": 3192 + "startLine": 3211 }, { "endCharacter": 15, - "endLine": 3198, + "endLine": 3217, "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": 3198 + "startLine": 3217 }, { "endCharacter": 15, - "endLine": 3199, + "endLine": 3218, "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": 3199 + "startLine": 3218 }, { "endCharacter": 39, - "endLine": 3200, + "endLine": 3219, "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": 3200 + "startLine": 3219 }, { "endCharacter": 38, - "endLine": 3210, + "endLine": 3229, "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py", "message": "\"columns\" is not a known attribute of \"None\"", "rule": "reportOptionalMemberAccess", "severity": "error", "startCharacter": 31, - "startLine": 3210 + "startLine": 3229 }, { "endCharacter": 34, - "endLine": 3211, + "endLine": 3230, "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py", "message": "\"index\" is not a known attribute of \"None\"", "rule": "reportOptionalMemberAccess", "severity": "error", "startCharacter": 29, - "startLine": 3211 + "startLine": 3230 }, { "endCharacter": 82, - "endLine": 3213, + "endLine": 3232, "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py", "message": "\"copy\" is not a known attribute of \"None\"", "rule": "reportOptionalMemberAccess", "severity": "error", "startCharacter": 78, - "startLine": 3213 + "startLine": 3232 }, { "endCharacter": 62, - "endLine": 3647, + "endLine": 3666, "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": 3647 + "startLine": 3666 }, { "endCharacter": 68, - "endLine": 4027, + "endLine": 4046, "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": 4023 + "startLine": 4042 }, { "endCharacter": 13, - "endLine": 4056, + "endLine": 4075, "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py", "message": "No overloads for \"cdist\" match the provided arguments", "rule": "reportCallIssue", "severity": "error", "startCharacter": 19, - "startLine": 4051 + "startLine": 4070 }, { "endCharacter": 38, - "endLine": 4054, + "endLine": 4073, "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": 4054 + "startLine": 4073 }, { "endCharacter": 9, - "endLine": 4159, + "endLine": 4178, "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py", "message": "No overloads for \"pdist\" match the provided arguments", "rule": "reportCallIssue", "severity": "error", "startCharacter": 36, - "startLine": 4157 + "startLine": 4176 }, { "endCharacter": 42, - "endLine": 4158, + "endLine": 4177, "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": 4158 + "startLine": 4177 }, { "endCharacter": 9, - "endLine": 4236, + "endLine": 4255, "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py", "message": "No overloads for \"cdist\" match the provided arguments", "rule": "reportCallIssue", "severity": "error", "startCharacter": 15, - "startLine": 4231 + "startLine": 4250 }, { "endCharacter": 34, - "endLine": 4234, + "endLine": 4253, "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": 4234 + "startLine": 4253 }, { "endCharacter": 99, - "endLine": 4425, + "endLine": 4444, "file": "quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py", "message": "No overloads for \"pdist\" match the provided arguments", "rule": "reportCallIssue", "severity": "error", "startCharacter": 12, - "startLine": 4425 + "startLine": 4444 }, { "endCharacter": 79, - "endLine": 4425, + "endLine": 4444, "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": 4425 + "startLine": 4444 }, { "endCharacter": 55, diff --git a/quickadapter/tests/test_prediction_history.py b/quickadapter/tests/test_prediction_history.py new file mode 100644 index 0000000..2965d8e --- /dev/null +++ b/quickadapter/tests/test_prediction_history.py @@ -0,0 +1,216 @@ +"""Runtime contracts for FreqAI's native QuickAdapter prediction history.""" + +import tempfile +import unittest +from pathlib import Path +from types import SimpleNamespace +from unittest import mock + +import numpy as np +import pandas as pd +from freqtrade.enums import RunMode +from freqtrade.exchange import timeframe_to_seconds +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen + +from quickadapter.user_data.freqaimodels.QuickAdapterRegressorV3 import QuickAdapterRegressorV3 + +PAIR = "BTC/USDT" +MARKER = "_freqai_strategies_produced" + + +def model_config(path: str) -> dict: + return { + "user_data_dir": Path(path), + "timeframe": "5m", + "stake_amount": "unlimited", + "runmode": RunMode.DRY_RUN, + "exchange": {"pair_whitelist": [PAIR]}, + "pairlists": [{"method": "StaticPairList"}], + "freqai": { + "enabled": True, + "identifier": "quickadapter-runtime-regression", + "continual_learning": True, + "train_period_days": 1, + "backtest_period_days": 1, + "conv_width": 1, + "fit_live_predictions_candles": 2, + "feature_parameters": { + "include_timeframes": ["5m"], + "include_corr_pairlist": [], + "label_period_candles": 1, + "shuffle_after_split": False, + }, + "data_split_parameters": {"test_size": 0, "shuffle": False}, + "model_training_parameters": {"n_estimators": 2, "n_jobs": 1}, + "label_prediction": {"method": "none"}, + }, + } + + +class PredictionHistoryTest(unittest.TestCase): + def test_native_append_restart_and_causal_calibration(self): + with tempfile.TemporaryDirectory() as temp: + config = model_config(temp) + model = QuickAdapterRegressorV3(config=config) + model.live = True + dates = pd.date_range("2026-01-01", periods=4, freq="5min", tz="UTC") + strat_df = pd.DataFrame( + {"date": dates, "high": [101.0] * 4, "low": [99.0] * 4, "close": [100.0] * 4} + ) + dk = SimpleNamespace( + data={ + "extra_returns_per_train": { + "label_period_candles": 1, + "label_natr_multiplier": 1.0, + "holdout_rmse": np.inf, + } + }, + label_list=["&s-extrema"], + unique_class_list=[], + full_df=strat_df, + ) + bootstrap = pd.DataFrame({"&s-extrema": [21.0, 22.0, 23.0, 24.0]}) + model.set_initial_historic_predictions(bootstrap, dk, PAIR, strat_df) + model.fit_live_predictions(dk, PAIR) + model.dd.set_initial_return_values(PAIR, bootstrap, strat_df) + self.assertEqual(model.dd.historic_predictions[PAIR][MARKER].tolist(), [False] * 4) + self.assertEqual(dk.data["labels_mean"]["&s-extrema"], 0.0) + model.dk = SimpleNamespace(check_if_model_expired=lambda _: False) + + def produce(dataframe: pd.DataFrame, value: float, status: int) -> None: + with mock.patch.object( + model, + "predict", + return_value=(pd.DataFrame({"&s-extrema": [value]}), np.array([status])), + ): + model.build_strategy_return_arrays(dataframe, dk, PAIR, 0) + + produce(strat_df, 1.0, 0) + extended = pd.concat( + [ + strat_df, + pd.DataFrame( + { + "date": [dates[-1] + pd.Timedelta(minutes=5)], + "high": [101.0], + "low": [99.0], + "close": [100.0], + } + ), + ], + ignore_index=True, + ) + dk.full_df = extended + produce(extended, 3.0, 1) + model.fit_live_predictions(dk, PAIR) + self.assertEqual(dk.data["labels_mean"]["&s-extrema"], 2.0) + self.assertEqual(dk.data["labels_std"]["&s-extrema"], 1.0) + self.assertEqual(len(model.dd.historic_predictions[PAIR]), len(extended)) + + future = pd.concat( + [ + extended, + pd.DataFrame( + { + "date": [dates[-1] + pd.Timedelta(minutes=10)], + "high": [101.0], + "low": [99.0], + "close": [100.0], + } + ), + ], + ignore_index=True, + ) + dk.full_df = future + produce(future, 999.0, 1) + self.assertNotIn(MARKER, dk.return_dataframe) + dk.full_df = extended + model.fit_live_predictions(dk, PAIR) + self.assertEqual(dk.data["labels_mean"]["&s-extrema"], 2.0) + model.dd.save_historic_predictions_to_disk() + + restored = QuickAdapterRegressorV3(config=config) + restored.live = True + self.assertTrue(restored.dd.load_historic_predictions_from_disk()) + restored.fit_live_predictions(dk, PAIR) + self.assertEqual(dk.data["labels_mean"]["&s-extrema"], 2.0) + self.assertEqual(dk.data["labels_std"]["&s-extrema"], 1.0) + self.assertEqual(len(restored.dd.historic_predictions[PAIR]), len(future)) + self.assertEqual( + restored.dd.historic_predictions[PAIR][MARKER].tolist(), [False] * 3 + [True] * 3 + ) + + def test_backtest_does_not_use_future_model_but_resumes_from_earlier_artifact(self): + with tempfile.TemporaryDirectory() as temp: + config = model_config(temp) + pair = PAIR + + def frame(day: str, offset: float) -> pd.DataFrame: + values = np.arange(48) + return pd.DataFrame( + { + "date": pd.date_range(day, periods=48, freq="5min", tz="UTC"), + "%-feature": np.sin(values / 4) + offset, + "&s-extrema": np.cos(values / 5), + } + ) + + def kitchen(settings: dict, data: pd.DataFrame, *, live: bool): + dk = FreqaiDataKitchen(settings, live=live, pair=pair) + timestamp = int( + ( + data["date"].iloc[-1] + pd.Timedelta(seconds=timeframe_to_seconds("5m")) + ).timestamp() + ) + dk.set_paths(pair, timestamp) + dk.set_new_model_names(pair, timestamp) + dk.data_path.mkdir(parents=True, exist_ok=True) + dk.label_list = ["&s-extrema"] + dk.training_features_list = ["%-feature"] + return dk, timestamp + + source = QuickAdapterRegressorV3(config=config) + source.live = True + future = frame("2026-02-01", 1000.0) + future_dk, future_ts = kitchen(config, future, live=True) + deployed = source.train(future, pair, future_dk) + source.dd.get_pair_dict_info(pair) + source.dd.pair_dict[pair]["trained_timestamp"] = future_ts + source.dd.save_data(deployed, pair, future_dk) + + backtest_config = dict(config) + backtest_config["runmode"] = RunMode.BACKTEST + backtest_config["timerange"] = "20260101-20260105" + backtest_config["config_files"] = [ + "/workspace/quickadapter/user_data/config-template.json" + ] + backtest = QuickAdapterRegressorV3(config=backtest_config) + backtest.live = False + for day, offset, save_model in ( + ("2026-01-01", 0.0, False), + ("2026-01-02", 10.0, True), + ("2026-01-03", 20.0, False), + ): + training = frame(day, offset) + dk, timestamp = kitchen(backtest_config, training, live=False) + self.assertLess(timestamp, future_ts) + trained = backtest.train(training, pair, dk) + transformed = dk.data_dictionary["train_features"]["%-feature"] + if offset < 20.0: + self.assertAlmostEqual(float(transformed.min()), -1.0) + self.assertAlmostEqual(float(transformed.max()), 1.0) + else: + self.assertGreater(float(transformed.min()), 2.0) + if save_model: + backtest.dd.pair_dict[pair]["trained_timestamp"] = future_ts + backtest.dd.save_data(trained, pair, dk) + backtest = QuickAdapterRegressorV3(config=backtest_config) + backtest.live = False + backtest.dd.get_pair_dict_info(pair) + self.assertEqual(backtest.dd.pair_dict[pair]["trained_timestamp"], future_ts) + else: + backtest.dd.save_metadata(dk) + + +if __name__ == "__main__": + unittest.main() diff --git a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py index 623a22b..5df822a 100644 --- a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py +++ b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py @@ -450,7 +450,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): https://github.com/sponsors/robcaulk """ - version = "3.13.0-rc.11" + version = "3.13.0-rc.12" _CALIBRATION_START_KEY: Final[str] = "quickadapter_calibration_start" _DEPLOYMENT_COORDINATE_MARKER_KEY: Final[str] = "quickadapter_deployment_coordinates" @@ -2372,7 +2372,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): ) def _resolve_deployment_state( - self, dk: FreqaiDataKitchen, pair: str + self, dk: FreqaiDataKitchen, pair: str, *, as_of_ts: int | None = None ) -> tuple[Any, Pipeline, Pipeline] | None: """Restore the deployed model with independent copies of its fitted pipelines.""" if not self.continual_learning: @@ -2401,6 +2401,19 @@ class QuickAdapterRegressorV3(BaseRegressionModel): previous = self.dd.pair_dict.get(pair, {}) if model is None and not previous.get("model_filename"): return None + if not self.live: + # The drawer timestamp can advance without saving a model. Bound the + # actual artifact instead, before reading cached or persisted state. + name, separator, timestamp = previous.get("model_filename", "").rpartition("_") + if ( + as_of_ts is None + or not previous.get("trained_timestamp") + or not separator + or name != f"cb_{pair.split('/')[0].lower()}" + or not timestamp.isdecimal() + or int(timestamp) >= as_of_ts + ): + return None try: cached = self.dd.meta_data_dictionary.get(pair) if model is not None and cached is not None: @@ -2499,7 +2512,13 @@ class QuickAdapterRegressorV3(BaseRegressionModel): ) if not self.freqai_info.get("fit_live_predictions_candles", 0) or not self.live: dk.fit_labels() - deployment_state = self._resolve_deployment_state(dk, pair) + as_of_ts = None + if self.continual_learning and not self.live: + as_of_ts = int(dates.max().timestamp()) + timeframe_to_seconds(self.config["timeframe"]) + training_timerange = getattr(self, "training_timerange", None) + if training_timerange is not None: + as_of_ts = min(as_of_ts, int(training_timerange.stopts)) + deployment_state = self._resolve_deployment_state(dk, pair, as_of_ts=as_of_ts) dd = self._apply_pipelines( dd, train_weight_inputs, diff --git a/quickadapter/user_data/strategies/QuickAdapterV3.py b/quickadapter/user_data/strategies/QuickAdapterV3.py index 6ad1153..2a59bef 100644 --- a/quickadapter/user_data/strategies/QuickAdapterV3.py +++ b/quickadapter/user_data/strategies/QuickAdapterV3.py @@ -218,7 +218,7 @@ class QuickAdapterV3(IStrategy): _ANNOTATION_LINE_OFFSET_CANDLES: Final[int] = 10 def version(self) -> str: - return "3.13.0-rc.11" + return "3.13.0-rc.12" timeframe = "5m" timeframe_minutes = timeframe_to_minutes(timeframe) -- 2.53.0