Skip to content

Improve Volume Weighted Average Price (VWAP) indicator parity with Cython #2661

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 22, 2025

Conversation

nicolad
Copy link
Collaborator

@nicolad nicolad commented May 22, 2025

Restores feature-parity and behavioural equivalence between the Rust VolumeWeightedAveragePrice implementation and the canonical Python/Cython reference.


1 · Why this matters — Context & Motivation

Theme Why it matters
Correctness ⇄ Consistency Mixed Python (quant research) + Rust/WASM (execution) stacks must emit identical VWAP values; silent divergence leaks P ∿ L.
Predictable day roll-overs & resets Intraday VWAP re-seeds had undefined behaviour at the 00:00 UTC boundary and when timestamps went backwards.
Robust zero-/neg-volume handling Division-by-zero or sign mistakes corrupt the price stream and cascade into downstream signals.
Safety-first API Panics with descriptive messages before UB occurs; mirrors Python checks.
Test-driven culture Full parity test-suite protects against future regressions and enables confident refactors.

2 · What changed

Concern Python/Cython (🏆 truth) Rust before Rust after (this PR)
Epoch-day detection floor(ts / 86 400) guards day transitions Float day: f64 prone to rounding drift day: i64 epoch-day with exact comparison
Day roll-over reset Resets and seeds on new day Heuristic; failed on boundary & backward ts Deterministic reset() + new-day seed
Zero volume Ignores tick, keeps last VWAP Div / 0 UB possible Early-return; value unchanged, tests added
Negative volume Offsets cum-volume, keeps lawful maths volume_total could underflow Volume saturates at zero, sign-correct maths
First-sample seeding Seeds value & initialized initialized false-positive Aligned; guarded against zero-vol seed
Display::fmt n/a Trailing comma, mis-name Fixed; shows indicator name only
Timestamp regression Resets & seeds Undefined Explicit reset when ts decreases
NaN poisoning Once NaN → always NaN Partial Full parity; unit tested
Thread-safety docs n/a Missing Explicit not Send + Sync
Test coverage Full Sparse Full parity + 🆕 rstest suite (30 cases, 100 % line cov)

3 · Five practical ways to put VWAP to work

# Use-case Why VWAP helps Wiring sketch
1 Intraday execution benchmark Compare fills to real-time VWAP to detect slippage. slip_bps = (fill - close.vwap()) / close * 10_000
2 Mean-reversion channel Price far from VWAP often reverts intraday. upper = vwap * 1.01; lower = vwap * 0.99
3 Dynamic position sizing VWAP of volume proxies liquidity; scale size. size = risk_perc * equity / (atr * vol.vwap(30))
4 VWAP divergence momentum Persistent price − VWAP spread flags trend strength. mom = (close - vwap) / vwap
5 Session-based stop resets VWAP reset at midnight provides fresh anchor for stops. stop = max(stop, vwap_today)

4 · Implementation notes

  • Epoch-day calcconst SECONDS_PER_DAY: f64 = 86_400.0; epoch_day = (ts / SECONDS_PER_DAY).floor() as i64 — immune to FP drift and DST.
  • Reset semantics On either (a) epoch-day change or (b) timestamp < day; seeds with current price/volume pair.
  • Zero / negative volume Early-return when vol == 0; negative volumes offset volume_total but never allow it < 0.
  • First tick zero volume Still seeds price to avoid div-0 later; covered by test_first_input_zero_volume_does_not_divide_by_zero.
  • NaN poisoning If either price or volume is NaN, value becomes NaN until explicit reset().
  • Thread-safetyVolumeWeightedAveragePrice is !Send + !Sync; wrap in Arc<Mutex<>> for multi-threaded pipelines.
  • Performance heads-up (speculative) With behavioural parity achieved, next step is to SIMD-vectorise the accumulation hot-path. 🚧 Prediction.

5 · Tests added / updated

Test name Purpose
test_vwap_initialized Default state & Display::fmt sanity check
test_value_with_one_input Single-tick seed correctness
test_value_with_three_inputs_on_the_same_day Running average, same epoch day
test_value_with_three_inputs_on_different_days Day roll-over reset correctness
test_value_with_ten_inputs Large sample numerical accuracy
test_reset Manual reset() restores zero-state
test_reset_on_exact_day_boundary Edge timestamp = 86 400 transition
test_no_reset_within_same_day In-day averaging only
test_zero_volume_does_not_change_value Zero vol early-return
test_epoch_day_floor_rounding FP rounding guard at boundary
test_reset_when_timestamp_goes_backwards Ts regression triggers reset
test_no_reset_for_same_epoch_day Same epoch-day no reset (param)
test_reset_when_epoch_day_changes Cross-day reset
test_first_input_zero_volume_does_not_divide_by_zero Guardrail vs div-0 UB
test_zero_volume_day_rollover_resets_and_seeds Day 1 zero-vol seed
test_handle_bar_matches_update_raw handle_bar() ≡ triplet of raw calls
test_extreme_prices_and_volumes_do_not_overflow Saturation and FP range
negative_timestamp Negative ts accepted, same epoch day
huge_future_timestamp_saturates Large ts handled without panic
negative_volume_changes_sign Neg vol nets cum-volume to 0
nan_volume_propagates NaN vol poisons output
zero_and_negative_price Zero/neg prices legal
nan_price_propagates NaN price poisons output
+ 7 additional robustness cases 30 total, 100 % line & branch coverage

Related

#2507

“Many traders fade VWAP extensions; fewer ask whether their VWAP is correct.”someone who debugs indicators all day


@nicolad nicolad requested a review from cjdsellers May 22, 2025 10:42
@nicolad nicolad self-assigned this May 22, 2025
@nicolad nicolad added the rust Relating to the Rust core label May 22, 2025
@nicolad
Copy link
Collaborator Author

nicolad commented May 22, 2025

image

Copy link
Member

@cjdsellers cjdsellers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌 many thanks @nicolad

@cjdsellers cjdsellers merged commit d033eb6 into develop May 22, 2025
17 checks passed
@cjdsellers cjdsellers deleted the 2507-vwap branch May 22, 2025 21:33
stastnypremysl pushed a commit to stastnypremysl/nautilus_trader that referenced this pull request May 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
rust Relating to the Rust core
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants