fix(quickadapter): report zigzag pivots as positions and close the coverage gate (#280)
* fix(quickadapter): pin the real fractal-dimension behaviour and de-duplicate the natr defaults
Two findings from the post-merge review.
`_fractal_dimension` was reported degenerate, and the report is right:
`HL1` and `HL2` each span a HALF of the window while `HL3` spans all of it,
so `HL1 + HL2 <= 2 * HL3`, `D <= 1`, and the clip returns exactly 1.0 on every
reachable input — measured, max 0.99994 over 300000 windows. `alpha` is
therefore identically `exp(-4.6 * 0) == 1.0` and `frama` reproduces `close`
after its seed.
I first rewrote it to Ehlers' sub-range formulation. That was wrong twice
over: `range(0, period, period // 2)` with period 16 still yields two
segments, so the dimension stayed 1.0, and the new code raised on any window
shorter than `period`. Both reverted. Reconstructing that recursion from memory
is not something to ship in a coverage pass.
What lands instead is the honest form. The docstring records the invariant and
the measurement, names the paper formulation that is deliberately NOT adopted,
and the tests now ASSERT the real behaviour: `frama` reproduces `close`, and
the dimension is exactly 1.0 on trending, noisy and plateaued windows alike. The
degeneracy becomes a pinned fact rather than a silent surprise — a change that
made the dimension vary would have to update a test instead of passing unnoticed.
The period guard now also refuses values below 2, which it previously did not.
The second finding: `natr_period` and `natr_multiplier` were written down three
times with no canonical source, which `.github/copilot-instructions.md` forbids
outright. All three now derive from `DEFAULT_LABEL_NATR_PERIOD` and the
existing `DEFAULT_MIN_LABEL_NATR_MULTIPLIER`. Values are unchanged (14, 9.0), so
no behaviour moves. Verified: mutating either canonical constant now turns the
suite red, where before all four spellings drifted together unnoticed.
The test added in `
166d63c1` claimed a fixture "can see any of the three" of
the alpha constants. That was false — it is removed, replaced by the two
assertions above.
* test(quickadapter): make two data-split assertions discriminate
Both findings come from the post-merge review and both are now load-bearing.
The weight-recomposition test asserted only the lengths and mean == 1, so any
uniform vector passed, even though the production comment immediately above
names "silently uniformize" as the hazard it exists to prevent. It now asserts
the RATIOS: base is a strict ramp and the label weights are None, so a
recomposition is a pure rescale and the weights must reproduce the base
ratios, which np.ones cannot do. Verified: replacing the validation-split
composition with np.ones now fails with "Mismatched elements: 5 / 6".
The NaN-skip test could not discriminate the guard it names, because its
fixture keeps a clean second column whose value coincides either way. A new
case uses the only emitted column carrying a NaN, where the caller must get
None and fall back to the position-based purge; without the guard the NaN
reaches astype(np.int64) and raises IntCastingNaNError. Verified killed.
One caveat on how the first was verified: the repo holds several
_compose_train_weights_with_support calls at the same indentation, so the
first mutation attempt replaced the wrong call site and appeared to survive.
It only died once the mutant was anchored on the full statement.
* test(quickadapter): bound the inner validation count
The `test_size >= n_train_rows` guard was never reached: relaxing it to `>`
left all 967 tests green. It is the refusal an operator sees when the inner
validation tail is as large as the training rows remaining after the outer
holdout, so losing it turns a clear DependencyException into a numpy error
far from the cause.
The new case exercises the boundary itself — exactly eight rows, and one more —
because `>=` versus `>` is only distinguishable there. Verified killed.
* test(quickadapter): close two ways the coverage gate could be widened silently
The suite contract derived its production module list from two FLAT globs while
the measured tree is `quickadapter/user_data` recursively. The file comment
claims deriving from the tree "keeps this contract true when a source file is
added", which was false for any file outside those two directories: it reached
the coverage denominator and was subject to no contract at all. It now walks
the tree, and derives each module name from its own location — bare name under
`strategies`, dotted path anywhere else. Verified: a flat module added at
`user_data/helpers_probe.py` that fails on import now errors the whole
collection, where before nothing looked at it.
The floor test banned `omit` and nothing else. `omit` is only the first way to
shrink the denominator: `exclude_lines`, `exclude_also` and the partial_*
family are the same lever under other names. Measured on coverage 7.16.2,
exclude_lines, exclude_also and partial_branches are all [report]-only — it
REJECTS them under [run] — so the guard now checks the full set in the section
that honours them, and the contract test checks them in both. Verified:
adding `exclude_also = ^\s*raise\b` to [report] turns the floor test red.
* test(quickadapter): make the fallback warning carry its own diagnosis
Under support_policy=fallback there is no exception for the operator to read, so
the WARNING is the entire diagnostic — but the test only used assertLogs, which
proves THAT something was logged, not WHAT. Dropping a reason from the sparse
support gate still left the suite green on this path.
It now asserts the single record names the threshold that actually failed and not
one that merely happened to trip alongside it. Verified: removing the
effective_sample_size reason turns three tests red, this one included.
Also refreshes the recorded coverage measurement to 69.3 percent (floor 67, gate
exit 0).
* fix(quickadapter): report zigzag pivots as positions, not index labels
`ZigzagResult.indices` was documented at Utils.py:725 as "positions of detected
pivots in series" and consumed that way by `compute_label_weights`, which casts to
int and bounds the result by the row count. It was produced as `df.index.tolist()`
— index LABELS. The two coincide only on a zero-based RangeIndex, which is what
every fixture in this suite used, so the mismatch was invisible.
On what FreqAI actually passes it is not cosmetic. Reproduced in the QA image:
RangeIndex(0..n) -> OK, 2 non-zero weights
RangeIndex(500..500+n) -> 5/5 pivots dropped, every weight silently 0.0
DatetimeIndex -> TypeError: int() argument ... not 'Timestamp'
So any config with `freqai.label_weighting.strategy` other than "none" crashes
`set_freqai_targets` on the first candle of every pair under live and backtest,
and degrades to zero label weighting — with only a warning — on any offset
integer index. The bug predates this suite; the suite is what hid it.
`_zigzag` now appends `pos`, the series assignment uses `.iloc`, and both sites
carry the reason. All three index shapes now agree exactly.
The suite could not have caught this on its own, so two tests were added that can:
one asserts the pivot positions and the label values are invariant across a
shifted, a datetime and a string index; the other drives `compute_label_weights`
end to end and requires a non-zero weight at every pivot. One existing assertion
read `label.series.loc[position]`, which only worked because labels and positions
coincided, and now uses `.iloc`. Verified: reintroducing `df.index[pos]` fails
five tests.
* fix(quickadapter): correct the frama degeneracy claim and pin the period guard
The `_fractal_dimension` docstring asserted two things that are false, and I wrote
them. It claimed "the clip therefore returns exactly 1.0 on every path, so alpha
is identically exp(-4.6 * 0) == 1.0 and frama reproduces close".
The inequality holds — HL1 and HL2 each span a half window while HL3 spans all of
it, so D <= 1 — but the conclusion does not. `np.clip(D, 1.0, 2.0)` is a no-op
for a value already at or above the lower bound, so it passes a ratio of
1.
0000000000000002 straight through. Measured on a deterministic ten-bar window:
D = 1.
0000000000000002, alpha = 0.
999999999999999. `frama` reproduces `close` to
within a few ULP, not bit-exactly, and a short-circuit on `alpha == 1.0` would
never fire.
The docstring now says what is actually true, states the deterministic
counterexample, and says explicitly that nothing downstream may treat alpha as a
literal 1.0. The test named `..._is_exactly_one_on_every_window` asserted
`== 1.0` on three hand-picked windows that happen to land exactly on 1.0, so it
was pinning a rounding accident rather than the contract; it now asserts within
rounding, and a new test carries the counterexample that makes it honest.
Separately, the `period < 2` clause of the guard had no test. The existing case
only drives odd periods, which the evenness clause already catches, so the clause
could be dropped with a green suite — and dropping it hands the caller numpy's
"zero-size array to reduction operation maximum which has no identity" instead of
the documented domain error. Zero is even, so it is the only value that exercises
the clause on its own; that is now the case.
One note on the mutant: rewriting the clause to `period < 1` leaves the suite
green, and that mutant is equivalent, not surviving — period 1 is odd and already
refused. The clause is only killable by removing it, which turns two tests red.
* test(quickadapter): close the four remaining ways the coverage gate goes inert
The gate was declared trustworthy and none of it was enforced. Each of the four
below was verified by mutation, and each now turns the suite red.
A `# pragma: no cover` in a production file. coverage.py always applies a
default `exclude_list` matching that pattern, so it is the source-side twin of
the `omit` key guarded two tests above, and the source side was open. Measured:
one pragma on `_normalize_final_take_profit_state` moved QuickAdapterV3.py from
58.8 % to 60.2 % and the total from 69.3 % to 69.6 %, with every test green. At
2.3 points of headroom the move is repeatable at every uncovered `def`.
The `fail_under` value. `.coveragerc` and README.md:528 both say "raise only,
never lower"; `fail_under = 5` passed everything. The floor test now pins the
lower bound and leaves raising it free, which is the direction the policy allows.
The provenance annotation. It had to contain the word "measured"; it did not have
to contain a date, and `# measured whenever someone remembered` passed. It now
has to record a well-formed date and a real percentage, and that percentage has
to clear the floor it justifies — a floor of 70 annotated with a 69.3 %
measurement is a gate that cannot pass.
Whether the CI step propagates the result. `exit $status` is the only thing
turning a failing `coverage report` into a failing job, no test read the
workflow, and deleting that one line left the whole class green. Verified: with
`fail_under = 99` the step exits 2 with the line and 0 without it.
One residual, deliberate: an old but well-formed date such as 1970-01-01 still
passes. Refusing a date older than N months would make the suite fail on a
repository nobody has touched, which is a time bomb rather than a gate. The
annotation has to be checkable; whether it is current stays a reviewer's call.
* test(quickadapter): pin the volume-weighted efficiency ratio and realign the README
`volume_weighted_efficiency_ratio` is the sixth COMBINED_METRIC feeding label
importance, and it was the only one with no numeric assertion: every check either
mirrored `efficiency_ratio` or expected 1.0, which holds for ANY positive
weighting. The W_LEGS fixture is monotone, so its ratio is 1.0 under every
convention and cannot discriminate. Shifting the volume window by one candle —
same length, different slice — left all 968 tests green.
The new case uses a slow uptrend carrying a fast oscillation, so the legs are
short and genuinely non-monotone and the ratio drops well below 1.0. It
restates the documented convention as an oracle over every leg: each
close-to-close step is weighted by the volume of the candle it lands on, and the
pivot bar at the start of the leg is excluded from the total. It then asserts
that the one-candle shift a mutation introduces gives a different answer, so the
convention is load-bearing rather than an identity. Verified: the window shift
now turns three tests and one error red.
Worth recording, because it cost time: I first tried six explicit shapes for the
fixture and every one gave exactly 1.0, which looked like proof the metric was
constant and therefore dead. It is not — across 3401 legs of random walks, 2941
differ from 1.0 with a wide spread. Zigzag legs are usually monotone, so a
sampled fixture is the only way to reach the non-trivial branch.
The README paragraph the previous commit edited was 99 columns, the widest prose
line in a 190 KB file whose prose wraps at 79; the widest before was 84. Rewrapped.
The coverage-gate paragraph also claimed the floor test refuses "any omit", which
stopped being true two commits ago, and understated what is now enforced.
* test(quickadapter): name a fractal-dimension test after what it asserts, align docstrings
`test_a_framed_gap_stops_the_fractal_dimension_at_the_window_edge` was named for
a gap and a window edge its fixture cannot contain — it feeds a constant frame, so
the only branch it reaches is the zero half-range short-circuit. The name read as
if the early return were covered from a second direction, which it is not.
Renamed to the assertion it actually makes: a constant window yields a FINITE
fractal dimension rather than log(0) and a -inf alpha. That guard is real and
distinct from the neighbouring constant-series case, so the test stays; the
overlapping tail assertion went, and a NaN check was added to match the name.
Twenty of twenty-eight test modules carried the "; requires the Freqtrade QA
image." clause and eight did not, which is the kind of drift the repo rules call
incoherent. All twenty-eight now carry it.
Left alone on purpose: `test_prediction_history.py` and
`test_regressor_prediction_history.py` are near-synonym names, and the new one is
the better of the two — the pre-existing module is what reads as the odd one out.
Renaming it would break a stable public path for a naming preference.
* test(quickadapter): guard the positional pivot contract at the strategy level
The previous commit proved the fix by hand — driving the real strategy path with
a DatetimeIndex frame and with a shifted integer index, both of which now give
184 labels and 23 non-zero extrema weights. That evidence was not kept, and the
suite still could not catch a regression: every fixture in the suite builds `date`
as a COLUMN and leaves a zero-based RangeIndex, which is exactly the shape that
hides the bug.
This runs `set_freqai_targets` on the real strategy with the index set both ways
and requires identical labels and identical weights, plus the absolute counts so
that a column of zeros or an all-positive column cannot satisfy it. Note the
weight count is 195, not 23: 23 of those sit on pivots and the rest are the
non-causal baseline fill.
Verified: restoring `.loc` at Utils.py:846 turns eight tests red, and appending
`df.index[pos]` turns five red.
* test(quickadapter): close the gaps the second review tour found
Every item below was confirmed by mutation before and after. Three were defects in
the previous tour's own work, and are called out as such.
The production default had no coverage at all. `_make_train_test_split_datasets`
is what a stock configuration runs — `causal_mode` defaults to True, the split
method defaults to `train_test_split`, `config-template.json` ships
`"test_size": 0.333` — and no test called it. Every split test drove
`_add_validation_split` or `test_size: 0` instead, so the whole causal purge block
ran zero times and both of its boundary comparisons were unguarded: relaxing `<`
to `<=` on the label horizon, or on the label's known_at position, kept the suite
green. Four cases now drive the outer split and pin each boundary from both sides,
including the counterpart with `causal_mode` off so they cannot pass for the wrong
reason. Both off-by-one mutants are killed.
The monotonic pivot watermark had an untested term. `add_pivot` takes the max of
four, and dropping `confirmed_at_pos` left the suite green while publishing labels
before their confirmation candle on 41 of 1500 seeded frames, by up to 96 candles.
The existing case cannot tell the term from `natr_warmup_end_pos` because on its
fixture the two coincide. A flat stretch followed by a trend separates them.
`compose_label_lookahead` had no test whatsoever. Its right-edge clamp was a row
wide in the wrong direction without anything noticing.
Three of these are mine. `test_the_workflow_propagates_the_coverage_result` scoped
its `exit $status` search on an unindented `- name:`, which never occurs in the
file, so it ran to end-of-file and a decoy token anywhere later satisfied it; it
now scopes on the step's own indent. It also never checked that the workflow points
at `.coveragerc` at all, so deleting one `--env` line left coverage with no
configuration, measuring the standard library and exiting 0. And the pragma guard's
comment claimed the tree was free of coverage's default exclusions when
`if TYPE_CHECKING:` is present in Utils.py; the guard now counts all three
DEFAULT_EXCLUDE patterns and pins the legitimate one.
Also: `include` was missing from the exclusion-key list, so one `[report] include`
line cut the denominator from 6217 statements to 2717 silently. `precision` was
checked for presence but never for value, and coverage's predicate is
`round(total, precision) < fail_under`, so `precision = 0` let a run half a point
below the floor pass.
And four tests asserted nothing: a tautological `assertLess` over a slice that
made it true by construction, now a behavioural test that the renamed section
reaches its keys through the second hop; a round-trip-only flat-config test that
any invertible pipeline satisfies, now asserting the resolved standardization and
normalization; a memoisation test that could not see a leak because each probe
rotates the timestamp and wipes the cache, now asserting after every refusal; and
`assertIsNotNone(import_module(...))`, which never returns None, deleted.
* docs(quickadapter): correct the coverage-gate citations and complete the guard list
Two comments pointed at README.md:528 for the "raise only, never lower" policy,
which is on line 532; 528 is the start of the surrounding paragraph. A line-number
citation into a file the same change edits is worth getting right.
The README paragraph listing what the floor test refuses named nine of the guards
and omitted three, one of which matters: it never said that `fail_under` drifting
into the inert `[run]` section is refused, so a maintainer reading it could "fix" a
red build by moving the line and believe the gate still holds. The list now covers
all sixteen, including the ones added since it was written.
Recorded coverage moves to 69.6 percent (floor 67, gate exit 0), from the same
configuration as the 69.3 percent it replaces.
* docs(quickadapter): correct the documented failure mode of an inline env prefix
README.md:518 said that prefixing `COVERAGE_RCFILE` and `COVERAGE_FILE` on a
single command makes `coverage report` "read no configuration and exit 0 while
measuring nothing". Reproduced in the QA image on a clean checkout: it prints
`No data to report.` and exits 1. The second command looks for the data file in
its default location, finds none, and fails.
The advice to export both variables is right, and the failure is loud rather than
silent — it cannot ship a green gate on a measurement that never happened — so the
text now says that instead. What it cannot do is send someone looking for a silent
zero exit that does not happen.
* test(quickadapter): pin how the two causal masks combine, and the flat-candle refusal
Both survivors were in tests added two commits ago, found by the third tour.
The causal purge applies two masks and the first version of that class proved
each one separately while never proving they combine: the horizon case carried no
availability column, the availability case used a zero horizon, so `keep_mask &=`
never ran with both constraints live. Rewriting the conjunction as an assignment
discards the horizon mask and kept the suite green. The new case exercises both
directions, because either alone is satisfied by a mask that simply overwrites the
other — the horizon removing rows the availability mask keeps, and the availability
mask removing a row the horizon kept.
I first wrote that case backwards, choosing a row the horizon mask keeps and
asserting it was purged. The reviewer caught it before it was committed. The
lesson is recorded in the test: pinning constraints one at a time says nothing
about how they combine.
The flat-candle threshold refusal was never checked against `_candle_threshold_cache`.
Every later probe rotates the timestamp on purpose, so a refusal that memoised was
wiped by the next invalidation and unobservable at the end of the test. It is now
asserted while that frame's signature is still live. Verified: inserting a memo in
that branch — with either `0.0` or `np.nan` — turns two subtests red, where before
both survived.
* test(quickadapter): reach the validation modes and weighting metrics that no test did
A block that never executes is worse than a weak assertion, because it reads as
coverage. The non-finite scalar case put the warn and none modes inside the same
`with` block as the raising call, so the raise on the first line unwound the block
and neither branch ever ran. The test name claimed all three modes. Split into
three, and the two mutants the reviewer measured — removing the non-finite
warning, and turning its `return None` into `return 0.0` — now both fail.
The per-metric weighting strategies had no test at all. Only `uniform` was ever
selected, so `weights = np.asarray(metrics[strategy])` never ran and replacing it
with `np.ones` left the suite green while every configured swing importance became
uniform. The amplitude strategy is reachable from a real config and is the
alternative the shipped template documents. The new cases pin the metric on the
pivots and the ratio the training-sample-weight consumer actually sees, since
the mean and positivity hold either way.
The reversal decay test asserted only that the decayed maximum is at least the
decayed minimum. That ordering follows from the earlier rejection of `lo > hi`,
so it holds for any input and says nothing about the decay; dropping the decay on
the maximum, or squaring the factor, both survived. The historical maximum is now
pinned at [1.0, 0.5, 0.25].
The eval weight test asserted a unit mean, which a binarisation of the label
weights also satisfies. It now pins the ratios.
And the migration table has two-hop chains that a structural "a chain exists"
assertion plus one behavioural chain did not cover: `threshold_outlier` reaching
`outlier_quantile`, `extrema_fraction` reaching `keep_fraction`, and a key under
the renamed `predictions_extrema` section reaching `threshold_method`. Sorting the
table by old path — which strands all three — now fails.
* test(quickadapter): guard the CI conditions that dispatch the gate, and a logger leak
Three passes closed the ways the coverage CONFIGURATION could be made inert. This
closes the level above it: whether the step runs at all. Flipping the QuickAdapter
matrix entry to `coverage: false`, or the step's own `if:` to a constant, left every
other test green while CI stopped running the gate — and a job that skips the
coverage step is indistinguishable from a green one. The matrix and the step
condition are now parsed rather than grepped, so an entry that loses its key, or an
`if:` that stops referencing the matrix, fails here too. All three mutants die.
`recorded_warnings` set its logger to NOTSET and never restored the level. Two
distinct problems, and only the second is fixed by restoring it. NOTSET makes the
logger inherit the root level, and a record the logger has already filtered out
never reaches a private handler: with the root at ERROR, all 45 cases in that file
failed on empty sinks. The capture level is now set explicitly, and the previous
level is restored on exit so later tests do not inherit it. Verified: 89 failures
before, 0 after.
* test(quickadapter): cover the chronological split, whose whole body never ran
`_make_timeseries_split_datasets` is reachable through the documented
`data_split_parameters.method='timeseries_split'`, and coverage reported 0 of its
62 statements executed. Its own availability comparison at
QuickAdapterRegressorV3.py:3108 could go from strict to inclusive, or be deleted,
with the suite green — a training row whose label is first knowable on the first
test row would be admitted into the fit.
It is a sibling of the block already pinned, not a subclass of it. Inheriting
looked like the economical choice and was wrong: the parent's expectations are
written around a ten-row test tail starting at position 30, where this last fold
is five rows starting at 35, and five inherited tests failed asserting the wrong
geometry. Each carries its own fixtures now, and the docstring records why.
The boundary needed measuring rather than assuming. `gap=2` removes the two rows
before the test fold from the training set BEFORE the availability comparison, so
the last row that comparison can act on is 32, not 34: measured train ends at 32
and test is 35..39. A lookahead placed on 33 or 34 is indistinguishable from no
lookahead at all. The cases now walk the last train row across availability at
32, 33 and 34 — all strictly before the cutoff — and exactly at 35, which is
purged. Verified: relaxing `<` to `<=` turns two tests red.
Coverage moves to 70.5 percent (floor 67, gate exit 0).
* test(quickadapter): pin the requested threshold method and the take-profit lower bound
Two survivors from the third tour, both in tests written before this review.
A range check cannot tell delegation from substitution. Every skimage threshold
method lands inside [-1, 1] whatever it computes, so forcing `threshold_func` to
Otsu inside the consumer left the whole suite green while `mean` silently became
Otsu's -0.9485 instead of -0.55 — an entry gate using a method the config never
asked for. The full tuple is now pinned per method. The whole table, not just the
differing entry: on this fixture `yen` coincides with `otsu` and `li` with `mean`,
so pinning a single method would leave the substitution open from the other side.
Verified: rerouting every method to Otsu, and rerouting only `yen` to `mean`, each
turn six subtests red.
The closed-interval case for the exit distances called only the stoploss helper,
so the take-profit endpoints were untested. Relaxing its valid condition to a
strict lower bound, or returning None for a zero fraction before the range check,
each left the suite green. Both helpers are now called at both endpoints. The upper
endpoint was already covered by sibling tests; the lower one is only observable
here, and the strict-lower mutant now dies.
test(quickadapter): cover the strategy business logic and gate coverage at 67% (#279)
Adds a unit-test suite for the QuickAdapter strategy covering the testable business logic of all five source modules, and an absolute coverage gate enforced in CI.
Branch coverage of quickadapter/user_data goes from 23.9% to 69.2% under the shipped configuration, with an enforced floor of 67%. The before column is main's single test file run against the same config, so both share one denominator; without include_namespace_packages the unexecuted trees vanish from the report entirely and the gate measures only what happened to be imported.
Layout is one file per business concern over a shared qa_support helper. The suite contract derives every production module name from the tree and refuses a second import identity for a strategies module, because a dotted import would shadow the bare one production uses and carry its own copy of the module globals.
Each file was checked against injected mutants of its own source; an independent adversarial review found four survivors the per-commit notes had mislabelled as equivalent or dead, all now covered and verified in both directions.
style(reforcexy): harmonize all log/error message prefixes to '<Component> [<scope>]:' (#264)
Exhaustive audit of ReforceXY.py messages: 144/148 now follow the
dominant '<Component> [<scope>]:' convention.
Ported (QuickAdapter PR #261) messages reprefixed:
- Training [%s]: cannot continue / continuing deployment / RL
preprocessing / model-on info log / warmup (Live [%s]:).
- Training banners converted to 'Training [%s]: starting|completed'
(were asymmetric '----Starting/done training----' separators).
Pre-existing non-scoped messages reprefixed:
- Repair [global]: FreqAI prediction repair synchronous-drawer guard.
- Journal [%s]: corrupt-journal quarantine warning.
- Hyperopt [global]: best-params lock/path filesystem invariants.
Remaining 4 are by-design, not user-facing message-convention items:
- 3 internal 'persisted ...' ValueErrors are immediately re-raised by
the conforming 'Training [%s]: cannot continue ...' DependencyException
wrapper; prefixing them would double-scope the surfaced text.
- FileExistsError(path) carries no literal message.
Line-count neutral: BasedPyright ReforceXY snapshot unaffected.
Merge pull request #259 from jerome-benoit/fix/forcexy-fit-live-predictions-validation
fix(reforcexy): validate optional prediction statistics window
refactor(quickadapter): replace duplicated domain string literals with canonical constants
chore(basedpyright): drop resolved reward_space_analysis ExtensionArray.size diagnostic
pandas-stubs 3.0.5.260914 resolves the previously-recorded
reportAttributeAccessIssue on ExtensionArray.size at
reward_space_analysis.py:2720, so the accepted ReforceXY snapshot no
longer matches current diagnostics. Remove the stale entry to unblock
the BasedPyright (ReforceXY) required check.
Merge pull request #256 from jerome-benoit/fix/quickadapter-rpc-custom-data-cleanup
fix(freqai): preserve prediction history and release annotation sessions
Merge pull request #254 from jerome-benoit/feat/renovate-basedpyright-snapshot-version
fix(qa): tolerate tool version drift in snapshot comparison