Change VecDeque for fixed-capacity ArrayDeque in SMA #2666
Merged
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.
Why touch the buffer at all?
SimpleMovingAverage
slides over a window whose length (period
) is known up-front.With the existing
std::collections::VecDeque
the queue can still grow past that length: eachpush_back
that exceedscapacity
triggers a heap re-allocation and mem-copy.VecDeque
is explicitly documented as “a double-ended queue implemented with a growable ring buffer”.Those reallocations are (a) wasted work—because we immediately
pop_front
afterwards—and (b) a latency spike on the trading hot-path.arraydeque::ArrayDeque
, on the other hand, is “a circular buffer with fixed capacity … [that] can be stored directly on the stack”.Its capacity is a const generic baked into the type, so pushes never allocate and never exceed the logical window.
VecDeque vs ArrayDeque at a glance
VecDeque
ArrayDeque<N>
const N
)period > N
usize::MAX
(practically unbounded)MAX_PERIOD = 1024
)Wrapping
(overwrite) orSaturating
(reject)What this PR does
buf: VecDeque<f64>
withbuf: ArrayDeque<f64, MAX_PERIOD, Wrapping>
.assert!(period ≤ MAX_PERIOD)
to keep invariants honest.buf.len() == count
on every tick.SimpleMovingAverage
stays the same struct/trait combo.Trade-offs & future considerations
period
they must re-compile with a biggerMAX_PERIOD
or make it a const-generic parameter.MAX_PERIOD × f64
bytes on the stack (≈ 8 KiB with the current 1024 cap). For deeply nested indicators we may revisit this.no_std
:arraydeque
already offers a#![no_std]
build—handy for embedded users if we ever target bare-metal.Why did the momentum tests (
bias.rs
,macd.rs
) start red-lining?Short answer: they were comparing bit-patterns, not behaviour.
The SMA refactor swaps re-summing the deque on every tick for a running sum that adds the new price and subtracts the price that just rolled out of the window. Same maths, but the order of floating-point operations changes, so the last ≈ 1 × 10⁻¹⁵ of the result can flip.
VecDeque
)ArrayDeque
)sum = inputs.iter().sum()
— re-sums N numbers each tick. Stable reduction order.sum += price; sum -= oldest;
— incremental update. Different reduction order ⇒ different rounding noise.crates/indicators/src/momentum/bias.rs
*Old test
with a default tolerance of ~1 × 10⁻¹⁴.
New SMA differs by ~6 × 10⁻¹⁶ (≈ 1 ULP).
Fix — keep the analytical target but make the bound explicit and self-documenting:
crates/indicators/src/momentum/macd.rs
assert_eq!
on-2.500_000_000_016_378e-5
.approx_equal
helper with a 1 × 10⁻¹² ring-fence.Why this is not hiding a logic bug
Take-away for future tests
assert_eq!(f64, f64)
unless checking forNAN
,INFINITY
, etc.