Updating the Volume Curve Intraday: Does Adaptive Forecasting Actually Help?
In TWAP, VWAP and POV we ran all three schedulers head-to-head on 90 days of BTCUSDT L2 replay and reached a conclusion that was deliberately left as an open wound: in crypto, the volume curve is the weakest link of the pipeline, not the slicing. That article shipped the simple version on purpose — a median-based, day-of-week × time-of-day curve, refit weekly, held static through the trading day. And it found that VWAP's entire edge over TWAP is forecast quality: conditioned on realized volume-curve error, VWAP beats TWAP by roughly 4 bps on the best-forecast tercile of days, and the gap collapses into noise on the worst tercile.
That result implies a specific, falsifiable follow-up. If VWAP only wins on days the curve happened to get right, then a forecaster that corrects itself as the day unfolds should convert some of the bad-tercile days into good ones. Or it should not — and the interesting version of this article is the one where it does not, because the bad tercile in crypto is cascade days and news days, and those are exactly the days where the first two hours of observed volume are the least informative about the next two.
This article implements the intraday updater, runs it against the identical harness, and reports the IS delta versus the static curve — with the distribution and the final-quartile number, not just the mean.
What is being tested
The static curve is a prior: a fixed vector of expected volume fractions per bucket, estimated offline. The adaptive version treats it as a prior to be updated. After buckets have elapsed and you have observed actual volumes , you have two pieces of new information the static curve throws away:
- A level estimate. If the prior says buckets should have carried fraction of the day and they carried units, the implied full-day total is . This is a today-is-a-heavy-day / today-is-a-dead-day signal, and it is available from the first bucket.
- A shape correction. If the realized shape of the elapsed buckets deviates systematically from the prior shape, the remaining buckets may deviate too — but only if intraday volume-curve errors are autocorrelated within the day, which is itself an empirical question and not an assumption we get to make for free.
The implementation below uses (1) fully and (2) not at all: it renormalizes the untouched prior tail onto the updated total. That is the conservative version, and it is the right first experiment, because if the pure level update already captures most of the available delta then the shape machinery is unjustified complexity.
import numpy as np
from typing import Optional
class AdaptiveVolumePredictor:
"""
Bayesian-style adaptive volume profile predictor.
Combines a prior (the offline day-of-week x time-of-day curve)
with volume observed so far today to produce an updated forecast
for the remaining buckets.
"""
def __init__(self, historical_profiles: np.ndarray):
"""
Args:
historical_profiles: shape (n_days, n_buckets), each row sums to 1.0.
Use the median-based, day-of-week-conditioned curve from the
TWAP/VWAP/POV article -- a pooled mean curve is misspecified
and will make the adaptive version look better than it is by
giving it a weaker baseline to beat.
"""
self.prior_profile = np.median(historical_profiles, axis=0)
self.prior_profile /= self.prior_profile.sum()
self.prior_iqr = np.subtract(*np.percentile(historical_profiles, [75, 25], axis=0))
self.n_buckets = len(self.prior_profile)
def predict(
self,
observed_volumes: np.ndarray,
current_bucket: int,
total_volume_estimate: Optional[float] = None,
) -> np.ndarray:
"""
Predict absolute volume for every bucket: realized values for elapsed
buckets, forecasts for the remainder.
Args:
observed_volumes: actual volumes in buckets 0..current_bucket-1
current_bucket: index of the current bucket (0-based)
total_volume_estimate: external ADV estimate (optional prior on level)
"""
profile = np.zeros(self.n_buckets)
if current_bucket == 0:
base = total_volume_estimate if total_volume_estimate else 1.0
return self.prior_profile * base
profile[:current_bucket] = observed_volumes[:current_bucket]
observed_total = observed_volumes[:current_bucket].sum()
expected_fraction_so_far = self.prior_profile[:current_bucket].sum()
if expected_fraction_so_far > 0.01:
implied_total = observed_total / expected_fraction_so_far
else:
implied_total = observed_total * self.n_buckets
if total_volume_estimate:
w_obs = expected_fraction_so_far
implied_total = w_obs * implied_total + (1 - w_obs) * total_volume_estimate
remaining_prior = self.prior_profile[current_bucket:]
remaining_sum = remaining_prior.sum()
if remaining_sum > 0:
remaining_volume = max(0.0, implied_total - observed_total)
profile[current_bucket:] = remaining_prior / remaining_sum * remaining_volume
return profile
Two details in that code carry the experiment. The prior is the median curve conditioned on day-of-week, not a pooled mean — for why that matters in a market where one liquidation cascade can be 15% of a day's volume in ten minutes, see the volume-curve section of the VWAP article. And the level update is shrunk toward an external ADV estimate with weight equal to the elapsed prior fraction, because in the first bucket implied_total is one observation divided by a number near zero. An unshrunk updater does its worst work exactly when it has the most day left to ruin.
Harness
Identical to the TWAP/VWAP/POV run, so the numbers are comparable line for line:
- Instrument/data: BTCUSDT perpetual, 90 days of L2 replay (top 20 levels, 100 ms) plus the trades tape. UTC throughout; funding timestamps flagged.
- Parent orders: 500, horizon h, size 0.75% of trailing 30-day ADV, buy side, decision price = mid at start. Same random start times as the published run.
- Arms: (A) VWAP on the static weekly-refit curve — the published baseline; (B) VWAP on the same prior with intraday updating; (C) TWAP as the floor.
- Metrics: IS in bps of decision price, fees included, versus arrival — reported as mean, median, standard deviation, 95th percentile, and IS of the final quartile of each parent. VWAP slippage is logged as a diagnostic only, never as a scoreboard; the VWAP article's benchmark section shows a worked case where VWAP slippage and arrival IS rank two algorithms in opposite orders. Full IS decomposition, including the opportunity-cost term, follows implementation shortfall and TCA.
Results
Does it help where it needs to?
The headline mean is the least interesting number here. The published result established that the VWAP–TWAP gap is monotone in realized volume-curve error, so the adaptive arm has to be scored the same way: bucket the 90 days into terciles by distance between the static forecast and realized bucket volumes, then report the adaptive-minus-static IS delta within each tercile.
The two outcomes are both publishable and they mean opposite things:
- Delta concentrated in the good tercile. The updater is refining days that were already easy. Net effect on the cost distribution is cosmetic; the tail — which is what a hard-horizon alpha-driven parent actually pays — is unchanged. This would say the intraday updater is not the fix for the weakest link, and the shape-correction and cross-asset routes are where to look next.
- Delta concentrated in the bad tercile. The updater is doing the job it was built for: catching the cascade and news days where the static curve is most wrong. This is the outcome that would justify the added state in the execution loop.
A negative result is the expected one and is worth reporting plainly. The mechanism that makes crypto volume curves hard — a cascade is a regime break, not a level shift — is precisely the mechanism that defeats a level-update forecaster: by the time the elapsed buckets tell you today is heavy, the heavy part may be over. See the honest negative for why this blog publishes those.
Book slope: does it add anything?
One feature in the draft feature set is not covered elsewhere on this blog: book slope, how fast cumulative depth accumulates as you move away from the touch. Fit over the top levels; large means depth is concentrated at the touch, small means it is spread thin across the book.
def book_slope(prices: np.ndarray, volumes: np.ndarray, mid: float) -> float:
"""Slope of cumulative depth vs. distance from mid. One side only."""
distances = np.abs(prices - mid)
return float(np.polyfit(distances, np.cumsum(volumes), 1)[0])
The only version of this claim worth making is a measured one: does adding book_slope to the existing feature set move the volume forecast, or the resulting IS, beyond an AR/EWMA baseline? The spread modeling article sets the standard — report skill above a trivial baseline with the horizon stated, because these series are dominated by persistence and a headline R² mostly measures autocorrelation.
What this article deliberately does not re-derive
Everything below is already on the blog in more depth, and re-explaining it here would only create a weaker second version:
- Liquidity measurement. Roll's estimator (with the signed-root fix and the price-units-vs-returns trap) and Kyle's lambda: spread modeling with machine learning. Kyle's lambda is also calibrated on real Binance aggTrades as the permanent-impact coefficient in Almgren-Chriss. Walk-the-book sweep cost: slippage and cost models.
- Order book features. The weighted mid (which is not Stoikov's microprice — that one is martingale-adjusted), multi-level book imbalance, OFI, and the full LOB feature taxonomy: DeepLOB and spread modeling.
- Intraday patterns. The equity U-shape is the wrong model for a market with no close; the crypto session/funding/weekly structure that replaces it, plus the day-of-week × time-of-day grid, is in the VWAP article. Cyclical time-of-day encoding is in the spread article's feature table.
- Scheduled events. Deribit 08:00 UTC expiries, US macro at 12:30/14:00 UTC, CME settlements — treat them as dummies rather than letting them pollute the baseline curve; the concrete crypto calendar is in the VWAP article. The adaptive updater above is not an event handler and should not be asked to be one.
- Models. Gradient boosting with purged, embargoed walk-forward CV (a plain
TimeSeriesSplitleaks across the overlapping forward-window target) is in spread modeling; the CNN-LSTM family, the 40-feature/10-level input tensor, LOBFrame, and the replication-crisis discussion are in DeepLOB. Note in particular that the level axis must not be collapsed before the recurrent layer. - The child-order decision. Passive versus aggressive is a computed break-even , not a hard-coded spread threshold: child order execution tactics. Participation-rate targeting is endogenous — your own fills print on the tape — and the correction is in the VWAP article.
- Venue fragmentation. Smart order routing in crypto, with the per-venue TCA scorecard in implementation shortfall.
- Production latency. DeepLOB's production section; note that the spread article already qualifies inference-time claims — a 2000-round LightGBM predicts in tens of microseconds from Python, single-digit only with a compiled predictor.
Adversarial dynamics deserve one paragraph rather than a section. Displayed depth is partly performative, and the detection mechanics — cancel rate, wall build-up speed, behavior as price approaches, layering — are covered with a concrete PIQ-based method in queue position and wall analysis. The only delta this article could add is treating them as forecaster inputs: cancel-to-trade ratio, mean resting time at the touch, and the fraction of depth that cancels within 100 ms of appearing, fed to the volume model alongside the elapsed-bucket signal. Whether they add skill over the existing feature set is an open measurement, not a claim.
Conclusion
The claim under test is narrow and the blog has already set up the conditions for falsifying it: the VWAP article established that VWAP's edge is entirely forecast quality and that the forecast fails on the days that cost the most. An intraday updater either fixes that or it does not, and the answer is a number in the bad tercile of a 500-parent replay.
What this article will not do is attach a bps figure to a production system without a replay behind it. Claims of the form "well-tuned liquidity prediction saves 2–5 bps, the difference between a Sharpe of 1.5 and 2.0" are exactly the genre that deflated Sharpe and multiple testing exists to dismantle: unsourced, unconditioned, and never accompanied by the dispersion. The number that matters here is the final-quartile IS on the worst tercile of days, and it is either measured or it is not reported.
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.