Skip to content

fix(ta): initialize recompute save-state in all indicator ctors (non-determinism UB)#52

Merged
luisleo526 merged 1 commit into
mainfrom
fix/ta-recompute-saved-state-init
Jun 30, 2026
Merged

fix(ta): initialize recompute save-state in all indicator ctors (non-determinism UB)#52
luisleo526 merged 1 commit into
mainfrom
fix/ta-recompute-saved-state-init

Conversation

@luisleo526

Copy link
Copy Markdown
Collaborator

Problem

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()) runs before the first compute() (compute() is the first thing to call save()), restore() reads indeterminate memory.

This surfaced 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_* 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 preceding compute() always re-sets saved_* via save() before any restore() 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 recomputecompute when empty were already safe.)

Validation

result
3commas triple-rsi-dca (1m-magnifier path) was non-deterministic 0 / 125 → now stably 125 (29% TV match, price-exact 100%); same .so 12× → one trade-CSV hash
corpus 251 excellent / 1 anomaly / 0 fail (byte-identical)
ctest 76/76 + test_recompute 102 checks: a poison-harness invariant (recompute-before-compute == compute-first for all 23 classes under 0xFF/0xAA/0x00 fill)
teeth reverting EMA's ctor init → EMA and TSI (embeds 4 EMAs) fail; reverting all → the invariant catches all 23
check_c_abi_runtime.py exit 0 (no new export)

🤖 Generated with Claude Code

…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>
@luisleo526 luisleo526 merged commit 643d69f into main Jun 30, 2026
5 checks passed
@luisleo526 luisleo526 deleted the fix/ta-recompute-saved-state-init branch June 30, 2026 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant