SIGNUM HQ 如何在數據源未提供 VWAP 時,以 1 分鐘 bars 計算交易時段 VWAP
How we compute session VWAP from 1-minute bars when the data feed does not give you one
SIGNUM HQ 改用已快取的 1 分鐘 bars 計算交易時段 VWAP,以常規交易時段內的典型價格乘成交量,再除以成交量總和,取代錯誤的日線近似值。計算只採用最新交易日的有效常規時段 bars,並處理週末、假期及過期快取,避免跨日混算或重複刷新。重用既有快取毋須額外呼叫數據供應商。
Our stock app showed a "VWAP" number for every ticker. For a while that number was a lie. This is a short write-up of how we found out, and how we now compute session VWAP from 1-minute bars when the data feed does not give you one.
The VWAP row on the stock command screen, now computed from 1-minute bars.
What was wrong
The market-data snapshot we use has a vwap field on the daily aggregate, but it is empty for a lot of tickers, during the session and on weekends. At some point someone (me) filled the gap with the usual shortcut from a daily bar:
vwap ≈ (high + low + close) / 3
That is the typical price of one bar. It is not a volume-weighted average of anything. It happens to land near the real VWAP on quiet days, which is why nobody noticed. On a day with a big gap and a late-session fade it can be off by a lot, and it was labelled "VWAP" in the UI.
We removed the fake value. The field then became null everywhere and the screen showed an em dash. Honest, but useless. So the real fix was to compute it.
The computation
VWAP for a session is the sum of price times volume divided by the sum of volume, over the regular session only. We already cache the 1-minute chart bars per ticker for the chart screen, so we reuse them and make zero extra calls to the data vendor.
// bars: 1-minute bars, each tagged with its ET date and session (PRE | REG | POST)
function sessionVwap(bars) {
const reg = bars.filter(b => b.session === 'REG' && b.volume > 0 && valid(b));
if (!reg.length) return null;
const day = latest(reg.map(b => b.etDate)); // newest trading day only
let pv = 0, vol = 0;
for (const b of reg.filter(b => b.etDate === day)) {
pv += ((b.high + b.low + b.close) / 3) * b.volume; // typical price x volume
vol += b.volume;
}
return vol > 0 ? pv / vol : null;
}
Pre-market and after-hours bars are excluded on purpose. They are thin, they print at odd prices, and most people mean "regular session" when they say VWAP.
The guards that took longer than the formula
The formula is five lines. These were the parts that actually broke:
1. During the session, show only today's value. An early version merged the previous trading day's value when today's bars were not there yet (right after the open, or when the cache was stale). A VWAP that silently mixes two days is worse than a dash. During market hours we now accept only bars with today's ET date, and otherwise return null.
2. Treating null from the source as authoritative wiped good data. On weekends the vendor returns vwap = null for every ticker. A change that said "if the source says null, show null" erased Friday's perfectly valid session VWAP. We reverted it. The rule is now: decide by the session date, not by whether a field is null.
3. Holidays are not weekends. On a weekday market holiday there are no new bars, but the "latest trading day" logic must still point at the previous session. We wired the exit check to a US market holiday calendar so a Monday holiday keeps showing Friday's VWAP.
4. Stale cache refresh without a stampede. If the minute-bar cache is old, we refresh it in the background rather than making the user wait. The first version used "read lock, then set lock", which has a race: two requests could both refresh. It is now a single atomic SET key NX EX 60 per ticker, and if the lock is not acquired we simply do not call the vendor.
Tests that mattered
- Empty bars (volume 0, null prices) are skipped, not treated as zero.
- A PRE-only or zero-volume series returns null, not 0.
- Two bars with known prices and volumes give an exactly checkable VWAP.
- A "closed" lookup time and a "last trade" time are separate inputs. One of our tests failed because it used the 20:30 ET query time as the trade time. The test was wrong, the product code was fine.
Takeaways
- A cheap approximation with the real name on it is a bug, even if it looks right most days.
- When a field is missing, decide what "missing" means (before open, holiday, vendor gap) before you decide what to display.
- Reusing data you already cache is the cheapest way to avoid new vendor calls and new rate limits.
This is from the data layer of SIGNUM HQ, a US-stock app I build solo (options flow, dark pool prints, earnings calendar).
Disclosure: I used an AI assistant to help draft and edit this post. The incident, the code and the conclusions are from my own project.
來源:DEV Community · dev.to
