fix(ta): initialize recompute save-state in all indicator ctors (non-determinism UB)#52
Merged
Merged
Conversation
…determinism UB) Indicator classes implement a save/restore mechanism for the engine's recompute() path (used by request.security HTF aggregation: a partial sub-bar issues recompute() -> restore()). Many constructors left their saved_* members UNINITIALIZED. When recompute() (-> restore()) is issued BEFORE the first compute() (-> which is the first call to save()), restore() reads indeterminate memory. This manifested on the 1m-magnifier + lookahead=barmerge.lookahead_on path: the security's first feed is a partial aggregated bar, so evaluate_security(..., is_complete=false) -> recompute() runs before any compute(). The live ta objects are freshly-constructed stack temporaries, so saved_* was uninitialized stack memory (not covered by value-init nor -ftrivial-auto-var-init). Garbage restored into the Wilder RMA poisoned RSI to NaN -> a DCA strategy produced 0 trades; a benign draw produced 125. The SAME binary flipped 0<->125 run-to-run. Initialize every saved_* member (ctor member-init or in-class initializer) to mirror the live member's initial value, so a recompute-before-compute restores a well-defined pristine state == compute() on bar 0. Byte-identical for every compute-first path (a preceding compute() always re-sets saved_* via save() before any restore() reads them) -> corpus unaffected. Classes fixed (23): RMA, RSI, EMA, Crossover, Crossunder, Cross, MFI, CMO, TSI, ATR, TR, Supertrend, DMI, SAR, Cum, AllTimeMax, AllTimeMin, BarsSince, OBV, AccDist, NVI, PVI, PVT, WAD, VWAP. (KC/ValueWhen and buffer-only classes that route recompute->compute when empty were already safe.) 3commas triple-rsi-dca on the 1m-magnifier path: was non-deterministic (0 or 125 trades); now stably 125 (29% TV match, price-exact 100%), same .so 12x -> one trade-CSV hash. Corpus held 251 excellent / 1 anomaly byte-identical; ctest 76/76 + test_recompute (102 checks): a poison-harness invariant asserting recompute-before-compute == compute-first for all 23 classes under 0xFF/0xAA/0x00 fill (reverting any ctor init makes it fail). No new C-ABI export. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Indicator classes implement a save/restore mechanism for the engine's
recompute()path (used byrequest.securityHTF aggregation: a partial sub-bar issuesrecompute()→restore()). Many constructors left theirsaved_*members uninitialized. Whenrecompute()(→restore()) runs before the firstcompute()(compute()is the first thing to callsave()),restore()reads indeterminate memory.This surfaced on the 1m-magnifier +
lookahead=barmerge.lookahead_onpath: the security's first feed is a partial aggregated bar, soevaluate_security(..., is_complete=false)→recompute()runs before anycompute(). The livetaobjects are freshly-constructed stack temporaries, sosaved_*is uninitialized stack memory (not covered by value-init nor-ftrivial-auto-var-init). Garbage restored into the Wilder RMA poisoned RSI to NaN → a DCA strategy produced 0 trades; a benign draw produced 125. The same binary flipped 0↔125 run-to-run — a non-deterministic backtest.Fix
Initialize every
saved_*member (ctor member-init list or in-class initializer) to mirror the live member's initial value, so a recompute-before-compute restores a well-defined pristine state ==compute()on bar 0. Byte-identical for every compute-first path (a precedingcompute()always re-setssaved_*viasave()before anyrestore()reads them), so the corpus is unaffected.23 classes fixed: RMA, RSI, EMA, Crossover, Crossunder, Cross, MFI, CMO, TSI, ATR, TR, Supertrend, DMI, SAR, Cum, AllTimeMax, AllTimeMin, BarsSince, OBV, AccDist, NVI, PVI, PVT, WAD, VWAP. (KC/ValueWhen and buffer-only classes that route
recompute→computewhen empty were already safe.)Validation
.so12× → one trade-CSV hashtest_recompute102 checks: a poison-harness invariant (recompute-before-compute== compute-first for all 23 classes under0xFF/0xAA/0x00fill)check_c_abi_runtime.py🤖 Generated with Claude Code