- 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:
"${{ 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:
- [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)
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
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
`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
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:
},
{
"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,
},
{
"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,
- [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)
- **`--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
- **`--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
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
**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
`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 |
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 |
| `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 |
| `*_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.
---
--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
_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(
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,
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
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):
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(
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
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:
current_open=current_open,
params=params,
),
- -0.15,
+ pnl_floor,
0.15,
)
)
"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),
}
)
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)]
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])
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(
)
# 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(
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 = {
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,
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())
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))
"""
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)
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",
) -> 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 = (
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:
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")
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")
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 = {
## 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
### 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
Columns:
-- ID: Stable identifier (`<category>-<shortname>-NNN`).
+- ID: Stable identifier (`<category>-<shortname>-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: <owning file>
```
-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
import unittest
from pathlib import Path
from typing import Any, cast
+from unittest import mock
import numpy as np
import pandas as pd
)
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(
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."""
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"
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()
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,
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(
)
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,
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,
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()
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):
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):
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(
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
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)
convert_optuna_params_to_model_params,
deepmerge,
)
+from ReforceXY.user_data.strategies.RLAgentStrategy import RLAgentStrategy
class RecordingPolicy:
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:
)
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)
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
)
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)
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,
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:
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)
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
)
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
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)
_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:
: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")
},
{
"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,
--- /dev/null
+"""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()
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"
)
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:
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:
)
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,
_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)