Weight each price by how long it prevailed instead of counting ticks. Keep the window's ticks in a queue with a running sum of price times duration between consecutive ticks. On a query, evict ticks that no longer prevail, trim the first interval to the window start and extend the latest price to now. O(1) amortized per tick.
mediumPattern: Sliding windowFrom Devana's Bloomberg question set
Problem
Ticks for an instrument arrive at irregular times: sometimes thousands a second, sometimes none for a minute. You need the time-weighted average price over the last five minutes, where each price counts for as long as it was the latest price.
A plain average of the last n ticks is wrong: a price that held for four minutes and one that lasted a second would count the same. Write a class with push(ts, price) and value(now), which returns the time-weighted average over [now - window, now].
class TimeWeightedAverage:
def __init__(self, window: int = 300): ...
def push(self, ts: int, price: float) -> None: ...
def value(self, now: int) -> float | None: ...This is one of Devana's own questions, written in the style of Bloomberg's interviews. It is not a question leaked from Bloomberg, and Devana is not affiliated with Bloomberg.
Constraints
- Each price holds from its tick until the next tick; the latest one holds until
now. - Timestamps passed to
pushandvaluenever decrease, andnowis at least the latest tick. - If the oldest tick still held is after the window start, average over the time since that tick.
- With no ticks, return
None; when the covered time is zero, return the latest price. - O(1) amortized per tick, with memory bounded by the ticks inside the window.
Examples
Example 1
Input: window = 300
push(0, 100); push(240, 200); value(300)
Output: 120.0100 held for 240 seconds and 200 for 60: (100 × 240 + 200 × 60) / 300 = 120. A simple average of the two ticks would say 150.
Example 2
Input: window = 300
push(0, 100); push(240, 200); value(600)
Output: 200.0The window is 300 to 600. The price of 100 stopped prevailing at 240, so only 200 counts, for all 300 seconds.
Hints
Open one at a time, and try the problem again after each.
Hint 1
Think of the price as a step function over time. The average is the area under it divided by the width of the window.
Hint 2
The area between two consecutive ticks never changes once the second tick has arrived. Can you keep a running total of those areas?
Hint 3
When can the oldest tick be thrown away? Not when its timestamp leaves the window, but when the tick after it is also at or before the window start.
Hint 4
Two pieces are partial at query time: the first interval is cut at the window start, and the latest price runs to now. Compute both on the fly.
Approach: from brute force to optimal
Brute force: recompute the area
Keep every tick in the window and, on each query, walk them summing price times duration, clipped to the window.
Complexity: O(1) per push, O(k) per query for k ticks in the window
A running area with a queue
Maintain area, the sum of price times duration over consecutive pairs of ticks in the queue. A push adds one term and an eviction subtracts one, so the sum is always current. A query adjusts only the two partial ends, which is O(1) once eviction is done.
Evict the front tick while the second tick is at or before the window start: from then on, the front price cannot prevail inside this window or any later one. Each tick is evicted once, so eviction is O(1) amortized.
Complexity: O(1) amortized per push and per query; O(k) space
How to say it in the interview
Describe the step function, even in words: "price against time; the average is area over width." That framing makes the eviction rule and the two partial ends obvious to the interviewer. Then state O(1) amortized, and raise floating-point drift in the running sum before they do.
Solution
Python
from collections import deque
class TimeWeightedAverage:
def __init__(self, window=300):
self.window = window
self.ticks = deque() # (ts, price), oldest first
self.area = 0.0 # sum of price * duration between consecutive ticks
def push(self, ts, price):
if self.ticks:
last_ts, last_price = self.ticks[-1]
self.area += last_price * (ts - last_ts)
self.ticks.append((ts, price))
self._evict(ts - self.window)
def value(self, now):
start = now - self.window
self._evict(start)
if not self.ticks:
return None
first_ts, first_price = self.ticks[0]
last_ts, last_price = self.ticks[-1]
begin = max(first_ts, start)
area = (self.area
+ last_price * (now - last_ts) # the latest price runs to now
- first_price * (begin - first_ts)) # cut the first interval at the start
span = now - begin
return area / span if span > 0 else last_price
def _evict(self, start):
# The front price no longer prevails once the next tick is at or before start.
while len(self.ticks) >= 2 and self.ticks[1][0] <= start:
(t0, p0), (t1, _) = self.ticks[0], self.ticks[1]
self.area -= p0 * (t1 - t0)
self.ticks.popleft()JavaScript
class TimeWeightedAverage {
constructor(window = 300) {
this.window = window;
this.ticks = []; // [ts, price], oldest first, from this.head
this.head = 0;
this.area = 0; // sum of price * duration between consecutive ticks
}
push(ts, price) {
if (this.size() > 0) {
const [lastTs, lastPrice] = this.ticks[this.ticks.length - 1];
this.area += lastPrice * (ts - lastTs);
}
this.ticks.push([ts, price]);
this.evict(ts - this.window);
}
value(now) {
const start = now - this.window;
this.evict(start);
if (this.size() === 0) return null;
const [firstTs, firstPrice] = this.ticks[this.head];
const [lastTs, lastPrice] = this.ticks[this.ticks.length - 1];
const begin = Math.max(firstTs, start);
const area = this.area
+ lastPrice * (now - lastTs) // the latest price runs to now
- firstPrice * (begin - firstTs); // cut the first interval at the start
const span = now - begin;
return span > 0 ? area / span : lastPrice;
}
size() {
return this.ticks.length - this.head;
}
evict(start) {
// The front price no longer prevails once the next tick is at or before start.
while (this.size() >= 2 && this.ticks[this.head + 1][0] <= start) {
const [t0, p0] = this.ticks[this.head];
this.area -= p0 * (this.ticks[this.head + 1][0] - t0);
this.head++;
}
if (this.head > 1024 && this.head * 2 > this.ticks.length) {
this.ticks = this.ticks.slice(this.head);
this.head = 0;
}
}
}Both versions are tested against the same cases, including every example on this page.
valueevicts too, because time moves on between pushes: a query long after the last tick must still drop prices that stopped prevailing.- The latest tick is never evicted. It is the price in force now, however old it is.
- A running sum of floats drifts after millions of additions and subtractions. In production, keep prices in integer units such as cents, or recompute the sum from the queue every so often.
Follow-up questions
Why is a count-based average wrong for this?
It weights every tick equally, so a burst of a thousand ticks in one second outweighs the price that held for the other four minutes. Time weighting counts each price for as long as anyone could trade at it.
How do you handle a period with no ticks at all?
The last price carries forward, so the average converges to it. If the instrument is halted, a stale price can mislead, so return the age of the last tick alongside the average and let the caller decide.
How would you extend this to a volume-weighted average?
Weight by traded volume instead of time: keep running sums of price times volume and of volume for the ticks in the window, evicted the same way from the front of the queue.
What changes with thousands of instruments in one process?
One instance per instrument in a hash map. Memory is the ticks from the last five minutes across all of them, so the busiest instruments dominate it.
Common mistakes
- Averaging the last n ticks, which ignores how long each price held.
- Evicting a tick as soon as its own timestamp leaves the window, when it may still be the price in force at the window start.
- Forgetting the open interval from the latest tick to
now. - Recomputing the whole window on every tick, which is O(k) when thousands arrive a second.
Practice it in a mock interview
Reading a solution and explaining one under questioning are different skills. The button below starts this problem as a voice mock interview with Josh, Devana's AI interviewer, with an editor that runs your code.
Practice this problem in a mock interview
Related problems
- Deduplicate a Stream of Retried Events: A hash set for membership and a queue in arrival order, so expired IDs fall out of the window.
- Realign Drifting Subtitles: Binary search for the anchor segment around each time, then interpolate along it.
- Sliding window: the pattern guide, with more practice questions.