Reconcile live pacing to a versioned remaining-volume forecast and expose any immediate catch-up quantity.
The decision this tutorial makes visible
A static profile can drift when today's realized volume differs from expectation; an adaptive schedule must update without using future observations.
The precise question is: Given realized market volume, a remaining forecast, and executed quantity, is the parent ahead or behind and how should future lots be reallocated?
A trader or execution analyst needs to understand what completion path the schedule requests and where it can lag. A builder needs the same parent, horizon, volume basis, forecast vintage, lot policy, and acknowledged execution state to reproduce the targets in code, audit data, visuals, and the browser lab.
Intuition before notation
Adaptive VWAP is a navigation update: compare where the parent is with where the newest point-in-time volume map says it should be.
The result depends on the parent decision clock, benchmark window, bucket calendar, eligible-volume definition, forecast availability time, executed-quantity finality, lot size, and rounding policy. Change any one of them and the schedule is a different instruction, even if it keeps the same strategy label.
Scope and nearby methods
The canonical algorithm combines realized volume known now with a versioned remaining forecast, calculates the target completed share, and places any behind-schedule catch-up in the first future bucket before allocating the rest by forecast weights.
| Variant | Definition | Best use | Main limitation |
|---|---|---|---|
| Canonical forecast reconciliation | Observed-to-projected share plus catch-up | Transparent adaptive teaching | No impact optimization |
| Trading envelope | Bound adaptive curve around static schedule | Limit abrupt changes | More parameters |
| Dynamic optimization | Optimize impact/risk with live signals | Advanced execution | Different objective |
What is sourced, selected, synthetic, and derived
| Role | Material claim | Evidence | Boundary |
|---|---|---|---|
| Sourced fact | Dang and Chen show that adaptive VWAP behavior can change participation across intervals and use a trading envelope; Konishi provides the static benchmark context; FIXatdl supports explicit strategy parameters. | S1, S2, S3 | The repository does not implement the paper's signal model or optimization. It uses a transparent point-in-time forecast reconciliation and concentrates catch-up in the first future bucket as a disclosed teaching choice. |
| Implementation choice | The canonical algorithm combines realized volume known now with a versioned remaining forecast, calculates the target completed share, and places any behind-schedule catch-up in the first future bucket before allocating the rest by forecast weights. | Frozen package definition | Child-order tactics, fills, impact, prices, fees, and venue behavior are excluded. |
| Synthetic teaching input | Parent orders, volume curves, forecasts, and fills are repository-authored. | datasets/canonical-input.json and scenario-results.json | They are not licensed executions or observed customer orders. |
| Author-derived calculation | The synthetic parent combines eight realized buckets with sixteen remaining forecast buckets, compares the 5,200 executed units with its lot-floored target, and reallocates every remaining lot with any deficit visible as first-bucket catch-up. | Formula, canonical fixture, Python/TypeScript parity, and independent arithmetic | Schedule fidelity does not establish benchmark outperformance or best execution. |
The sources establish benchmark, research, and interface context only. They do not certify this transparent allocator as a broker algorithm or validate its synthetic parent, curves, forecasts, fills, or results. The schedule is author-derived under the package-selected convention.
Formula, symbols, and numerical policy
target_done = lot_floor(Q·V_realized/(V_realized+V_forecast)); error = executed-target_done
| Symbol | Meaning | Unit | Policy |
|---|---|---|---|
| V_R | realized market volume known now | volume | completed buckets |
| V_F | remaining forecast volume | volume | versioned now |
| Q | parent quantity | units | fixed |
| E | executed quantity | units | acknowledged/final per caller |
| T_now | target completed now | units | lot-floored |
- Target completed is floored to a lot.
- Future allocation uses integer largest-remainder logic.
- Forecast version and availability time are required to prevent hindsight.
Read the formula in the same order as the algorithm. Validate identity, ordering, units, and supported state first. Apply the selected equality and window rules second. Calculate with unrounded numeric values. Round only at the declared presentation boundary, and preserve null as a diagnostic rather than coercing it to zero.
Build the algorithm
- Validate parent, realized data, forecast, executed quantity, and version
- Project total market volume
- Calculate lot-floored target completed now
- Classify ahead/behind and compute catch-up
- Allocate post-catch-up lots by remaining forecast
Production-minded operational checklist
- Freeze realized-volume inclusion rules
- Load the forecast available now
- Reconcile only final/accepted executions
- Bound catch-up through a downstream tactic
- Persist forecast and pacing decision for TCA
A schedule can be arithmetically exact and still be operationally unusable if its calendar, volume basis, forecast vintage, acknowledged fills, lot constraints, or downstream child tactic are wrong. Reject ambiguous inputs instead of manufacturing a precise trajectory.
Worked synthetic example
The canonical fixture is synthetic teaching data, not an observed control
event, customer order, or broker execution. Its primary author-derived output,
pacing_state, is the target completed quantity is 8,000, pacing error is −2,800, and a 2,800-unit catch-up is added to the first future bucket. The complete input and output
are in datasets/canonical-input.json and datasets/expected-output.json.
The synthetic parent combines eight realized buckets with sixteen remaining forecast buckets, compares the 5,200 executed units with its lot-floored target, and reallocates every remaining lot with any deficit visible as first-bucket catch-up.
Counterfactual checkpoint
Executed quantity equals the live target. Set executed quantity to the lot-floored target implied by realized volume and the current forecast. The output changes because The pacing error E minus T-now is exactly zero; no deficit remains to concentrate.
The structured result retains state and diagnostics in addition to the primary number. That makes the calculation independently reviewable and prevents a partial, null, rejected, or venue-bounded outcome from being mistaken for an unqualified value.
Boundary and counterexample workbook
The playground computes every scenario at 61 deterministic parameter states.
The table uses the declared focus step and states whether that focus reproduces
the canonical fixture. The full state ledger and compressed transition
segments are in datasets/scenario-results.json.
| Scenario | Review focus | Purpose | State | Primary output | Diagnostic | Decision segments |
|---|---|---|---|---|---|---|
| Canonical executed-quantity sweep | Step 32 · canonical fixture | Move acknowledged execution through behind, on-schedule, and ahead pacing states. Synthetic data; schedule output is not a fill guarantee. | adaptive-schedule | pacing error -2800 units | behind; catch-up 2800 | 2 |
| Exact on-schedule equality | Step 30 · comparison focus | Set executed quantity equal to the live lot-floored target; catch-up must be zero. Synthetic data; schedule output is not a fill guarantee. | adaptive-schedule | pacing error 0 units | on-schedule; catch-up 0 | 1 |
| Realized-volume surge | Step 30 · comparison focus | Increase completed-bucket volume and observe a higher target completed quantity. Synthetic data; schedule output is not a fill guarantee. | adaptive-schedule | pacing error -7700 units | behind; catch-up 7700 | 1 |
| Back-loaded remaining forecast | Step 30 · comparison focus | Shift forecast volume later and inspect future-lot reallocation. Synthetic data; schedule output is not a fill guarantee. | adaptive-schedule | pacing error 800 units | ahead; catch-up 0 | 3 |
| Light realized-volume case | Step 30 · comparison focus | Reduce observed volume so the same execution can move ahead of schedule. Synthetic data; schedule output is not a fill guarantee. | adaptive-schedule | pacing error 1600 units | ahead; catch-up 0 | 3 |
| Heavy realized-volume case | Step 30 · comparison focus | Increase observed volume so the same execution falls further behind. Synthetic data; schedule output is not a fill guarantee. | adaptive-schedule | pacing error -9400 units | behind; catch-up 9400 | 1 |
| Catch-up concentration risk | Step 30 · comparison focus | Create a large deficit and expose the package choice that places catch-up in the first future bucket. Synthetic data; schedule output is not a fill guarantee. | adaptive-schedule | pacing error -3000 units | behind; catch-up 3000 | 2 |
These rows are not backtest observations. They are controlled counterexamples that expose how one driver changes the state, output, or reason code while the rest of the contract stays fixed.
Visualize the boundary
Open this SVG at full size, or use the guided playground to compare the seven topic-specific canonical, boundary, policy, and failure scenarios.
The Mermaid flow answers where the selected calculation sits in the processing sequence. The SVG keeps the formula, output, decision boundary, and invariant visible together. The lab lets the reader step through the same structured states without changing the underlying definition.
Implementation walkthrough
The Python and TypeScript references validate parent and lot alignment, horizon or volume arrays, version identity, participation bounds, and executed-quantity state before allocating. They conserve the parent with integer lots and return every bucket, cumulative target, residual, and pacing diagnostic; they do not simulate orders or fills.
The main implementation branches are:
- executed < target — Mark behind and add catch-up, because Pacing deficit.
- executed = target — On schedule, because No pacing error.
- executed > target — Mark ahead; no catch-up, because Pacing surplus.
Neither reference silently fetches data, mutates caller-owned inputs outside the declared engine behavior, guesses hidden state, or substitutes a provider default. Shared JSON fixtures make value, null, state, and reason-code drift visible across languages.
Testing and validation
Definition tests compare every canonical field, reject malformed state, and exercise the material boundary. Family validation recomputes every playground state from the reference function. Independent arithmetic is recorded beside the fixture rather than inferred only from implementation output.
The audit must preserve these invariants:
- Executed plus scheduled future quantity equals the parent.
- Catch-up is zero when on schedule or ahead.
- Only realized buckets and the current forecast version influence the decision.
- The output retains
pacing_state, cumulative quantities, lot policy, and schedule state.
Passing these checks proves deterministic schedule construction and lot conservation under the selected inputs. It does not prove fills, benchmark tracking, best execution, lower transaction cost, market-impact control, or profitability.
Failure modes and misuse
- A target schedule is not an execution: it does not model child-order price, routing, queue position, fills, rejects, fees, or market impact.
- Tracking a schedule or benchmark does not prove best execution, reduced cost, predictive power, or profitability.
- Volume definitions, auction/off-exchange inclusion, corrections, time zones, calendars, and forecast versions must be explicit in production.
Debugging order
When a result looks surprising, inspect the state in this order:
- Confirm parent side, quantity, start/end window, trading calendar, and lot size.
- Confirm bucket count, eligible-volume definition, profile or forecast version, and availability time.
- Confirm executed quantity means acknowledged/final fills under the caller's policy.
- Recalculate lot conservation, cumulative targets, pacing error, and the declared scenario focus before changing code.
Evidence and historical boundary
Historical decision: deferred. A named real execution would require the parent decision time, side, instrument, benchmark window, contemporaneous consolidated market volume and prices, child acknowledgements and fills, fees, corrections, venue scope, clock alignment, and redistribution permission. Synthetic schedules make every allocation reproducible without suggesting benchmark success.
The primary sources are Dang–Chen dynamic VWAP, Konishi VWAP, FIXatdl 1.2 RC1. They support the source roles listed in the research ledger, not a redistributable historical observation, a customer execution, broker implementation, fill guarantee, benchmark outperformance, best-execution conclusion, profitability claim, or prediction claim.
Summary and next topic
You can now reforecast VWAP pacing without future-volume leakage. The learning flow is: Historical VWAP Execution → Adaptive VWAP Execution → Percentage-of-Volume Execution. Carry the result forward only with its scope, clock, state, and evidence label.
Rendered from the canonical Mermaid sources linked by this article.
Adaptive VWAP Execution calculation flow
This flow identifies the selected calculation stages and the structured output.
Takeaway: Adaptation changes only from information available now: realized volume, current fills, and a versioned remaining forecast.
ReferencesPrimary sources and evidence notesExpand the source trail, evidence role, and limitations behind the engineering choices.
Expand the source trail, evidence role, and limitations behind the engineering choices.
S1 — Dynamic Execution of VWAP Orders with Short-Term Predictions
- Organization or authors: Ngoc-Minh Dang and Yin Chen
- Source type: Original working paper
- Publication or effective date: 2013
- Version: SSRN 2366177
- URL or DOI: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2366177
- Accessed: 2026-07-30
- Jurisdiction: General execution research
- Supports: Adjusting participation across intervals instead of passively following one static curve, with bounded adaptive behavior.
- Limitations: The repository uses a simpler forecast-reconciliation rule and makes no empirical claim.
S2 — Optimal Slice of a VWAP Trade
- Organization or authors: Hizuru Konishi
- Source type: Original peer-reviewed research
- Publication or effective date: 2002-04
- Version: Journal of Financial Markets 5(2), 197–221
- URL or DOI: https://doi.org/10.1016/S1386-4181(01)00023-4
- Accessed: 2026-07-30
- Jurisdiction: General execution research
- Supports: Static VWAP execution slices and the importance of expected market-volume distribution.
- Limitations: The canonical package uses a transparent deterministic allocator, not the paper's optimization model.
S3 — FIX Algorithmic Trading Definition Language Online Specification
- Organization or authors: FIX Trading Community
- Source type: Official industry technical standard
- Publication or effective date: 2021-05-20
- Version: 1.2 Release Candidate 1
- URL or DOI: https://www.fixtrading.org/standards/fixatdl-online/
- Accessed: 2026-07-30
- Jurisdiction: Cross-market electronic trading interfaces
- Supports: Machine-readable strategy parameters, legal ranges, validation rules, effective/expire times, and sample POV/VWAP strategy identifiers.
- Limitations: Defines interfaces and validation, not a universal broker algorithm, schedule, fill model, or execution guarantee.
Evidence boundary
The sources establish only the named benchmark, strategy, research, or interface context. They do not verify the repository-authored parent order, profile, forecast, volume path, assumed fills, schedule, costs, or execution quality.
Full dependency-light reference implementations in both supported languages.
/* Deterministic reference algorithms for D13-F01 Schedule-Based Execution. */
type Inputs = Record<string, any>;
function integer(name: string, value: unknown, positive = false, nonnegative = false): number {
if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${name} must be an integer`);
if (positive && value <= 0) throw new Error(`${name} must be positive`);
if (nonnegative && value < 0) throw new Error(`${name} must be nonnegative`);
return value;
}
function weights(name: string, values: unknown): number[] {
if (!Array.isArray(values) || !values.length) throw new Error(`${name} must be a nonempty array`);
const parsed = values.map((value, index) => integer(`${name}[${index}]`, value, false, true));
if (parsed.reduce((a, b) => a + b, 0) <= 0) throw new Error(`${name} must contain positive total weight`);
return parsed;
}
function validateParent(totalQuantity: unknown, lotSize: unknown): [number, number] {
const total = integer("total_quantity", totalQuantity, true);
const lot = integer("lot_size", lotSize, true);
if (total % lot !== 0) throw new Error("total_quantity must be divisible by lot_size");
return [total, lot];
}
function allocateLots(totalQuantity: number, weightValues: number[], lotSize: number): number[] {
const totalLots = totalQuantity / lotSize;
const weightSum = weightValues.reduce((a, b) => a + b, 0);
const bases: number[] = [], remainders: Array<[number, number]> = [];
weightValues.forEach((weight, index) => {
const numerator = totalLots * weight;
bases.push(Math.floor(numerator / weightSum));
remainders.push([numerator % weightSum, index]);
});
let residual = totalLots - bases.reduce((a, b) => a + b, 0);
remainders.sort((a, b) => b[0] - a[0] || a[1] - b[1]);
for (let i = 0; i < residual; i += 1) bases[remainders[i][1]] += 1;
return bases.map((lots) => lots * lotSize);
}
export function twapExecution(
total_quantity: unknown, start_time_ms: unknown, end_time_ms: unknown,
bucket_count: unknown, lot_size: unknown,
): Record<string, unknown> {
const [total, lot] = validateParent(total_quantity, lot_size);
const start = integer("start_time_ms", start_time_ms, false, true);
const end = integer("end_time_ms", end_time_ms, true);
const buckets = integer("bucket_count", bucket_count, true);
if (end <= start) throw new Error("end_time_ms must be greater than start_time_ms");
if (total / lot < buckets) throw new Error("bucket_count cannot exceed available lots");
const allocations = allocateLots(total, Array(buckets).fill(1), lot);
const duration = end - start;
let cumulative = 0;
const schedule = allocations.map((quantity, index) => {
cumulative += quantity;
return {
bucket: index + 1,
start_time_ms: start + Math.floor(duration * index / buckets),
end_time_ms: start + Math.floor(duration * (index + 1) / buckets),
target_quantity: quantity,
target_cumulative_quantity: cumulative,
};
});
return {
total_quantity: total, lot_size: lot, bucket_count: buckets,
scheduled_quantity: cumulative, remaining_quantity: total - cumulative,
schedule, state: "complete-schedule",
};
}
export function historicalVwapExecution(
total_quantity: unknown, historical_bucket_volumes: unknown,
lot_size: unknown, profile_id: unknown,
): Record<string, unknown> {
const [total, lot] = validateParent(total_quantity, lot_size);
const volumes = weights("historical_bucket_volumes", historical_bucket_volumes);
if (typeof profile_id !== "string" || !profile_id.trim()) throw new Error("profile_id must be a nonempty string");
const allocations = allocateLots(total, volumes, lot);
const volumeSum = volumes.reduce((a, b) => a + b, 0);
let cumulative = 0;
const schedule = volumes.map((volume, index) => {
const quantity = allocations[index]; cumulative += quantity;
return {
bucket: index + 1, historical_volume: volume,
historical_weight: Math.round((volume / volumeSum) * 1e12) / 1e12,
target_quantity: quantity, target_cumulative_quantity: cumulative,
};
});
return {
profile_id: profile_id.trim(), total_quantity: total, lot_size: lot,
bucket_count: volumes.length, historical_volume_total: volumeSum,
scheduled_quantity: cumulative, remaining_quantity: total - cumulative,
schedule, state: "complete-schedule",
};
}
export function adaptiveVwapExecution(
total_quantity: unknown, realized_market_volumes: unknown,
remaining_forecast_volumes: unknown, executed_quantity: unknown,
lot_size: unknown, forecast_version: unknown,
): Record<string, unknown> {
const [total, lot] = validateParent(total_quantity, lot_size);
const realized = weights("realized_market_volumes", realized_market_volumes);
const forecast = weights("remaining_forecast_volumes", remaining_forecast_volumes);
const executed = integer("executed_quantity", executed_quantity, false, true);
if (executed > total || executed % lot !== 0) throw new Error("executed_quantity must be a lot-aligned value not exceeding total_quantity");
if (typeof forecast_version !== "string" || !forecast_version.trim()) throw new Error("forecast_version must be a nonempty string");
const realizedTotal = realized.reduce((a, b) => a + b, 0);
const forecastTotal = forecast.reduce((a, b) => a + b, 0);
const projectedMarketTotal = realizedTotal + forecastTotal;
const targetCompleted = Math.floor(Math.floor(total * realizedTotal / projectedMarketTotal) / lot) * lot;
const pacingError = executed - targetCompleted;
const remainingParent = total - executed;
const immediateCatchUp = Math.floor(Math.min(remainingParent, Math.max(0, -pacingError)) / lot) * lot;
const postCatchUp = remainingParent - immediateCatchUp;
const allocations = postCatchUp ? allocateLots(postCatchUp, forecast, lot) : Array(forecast.length).fill(0);
if (allocations.length) allocations[0] += immediateCatchUp;
let cumulative = executed;
const schedule = forecast.map((volume, index) => {
const quantity = allocations[index]; cumulative += quantity;
return {
future_bucket: index + 1, forecast_market_volume: volume,
target_quantity: quantity, target_cumulative_quantity: cumulative,
includes_catch_up_quantity: index === 0 ? immediateCatchUp : 0,
};
});
const pacingState = pacingError < 0 ? "behind" : pacingError > 0 ? "ahead" : "on-schedule";
return {
forecast_version: forecast_version.trim(), total_quantity: total, executed_quantity: executed,
remaining_quantity_before_schedule: remainingParent, realized_market_volume: realizedTotal,
remaining_forecast_market_volume: forecastTotal, projected_market_volume: projectedMarketTotal,
target_completed_quantity: targetCompleted, pacing_error_quantity: pacingError,
pacing_state: pacingState, immediate_catch_up_quantity: immediateCatchUp,
scheduled_future_quantity: allocations.reduce((a, b) => a + b, 0),
schedule, state: "adaptive-schedule",
};
}
export function percentageOfVolumeExecution(
total_quantity: unknown, participation_rate_bps: unknown,
market_volume_buckets: unknown, lot_size: unknown,
max_child_quantity: unknown,
): Record<string, unknown> {
const [total, lot] = validateParent(total_quantity, lot_size);
const rate = integer("participation_rate_bps", participation_rate_bps, true);
if (rate > 10000) throw new Error("participation_rate_bps cannot exceed 10000");
const volumes = weights("market_volume_buckets", market_volume_buckets);
let maxChild = integer("max_child_quantity", max_child_quantity, true);
if (maxChild < lot) throw new Error("max_child_quantity must be at least one lot");
maxChild = Math.floor(maxChild / lot) * lot;
let cumulativeMarket = 0, released = 0;
const schedule = volumes.map((volume, index) => {
cumulativeMarket += volume;
const rawTarget = Math.min(total, Math.floor(cumulativeMarket * rate / 10000));
const target = Math.floor(rawTarget / lot) * lot;
const desired = Math.max(0, target - released);
let child = Math.min(desired, maxChild, total - released);
child = Math.floor(child / lot) * lot;
released += child;
const achieved = Math.round((released * 10000 / cumulativeMarket) * 1e6) / 1e6;
return {
bucket: index + 1, market_volume: volume, cumulative_market_volume: cumulativeMarket,
target_cumulative_quantity: target, child_quantity: child,
released_cumulative_quantity: released, remaining_quantity: total - released,
achieved_participation_bps: achieved,
};
});
return {
total_quantity: total, participation_rate_bps: rate, lot_size: lot,
max_child_quantity: maxChild, observed_market_volume: cumulativeMarket,
released_quantity: released, remaining_quantity: total - released,
achieved_participation_bps: Math.round((released * 10000 / cumulativeMarket) * 1e6) / 1e6,
schedule, state: released === total ? "parent-complete" : "participating",
};
}
export function calculate(topicId: string, inputs: Inputs): Record<string, unknown> {
if (!inputs || typeof inputs !== "object" || Array.isArray(inputs)) throw new Error("inputs must be an object");
if (topicId === "D13-F01-A01") return twapExecution(inputs.total_quantity, inputs.start_time_ms, inputs.end_time_ms, inputs.bucket_count, inputs.lot_size);
if (topicId === "D13-F01-A02") return historicalVwapExecution(inputs.total_quantity, inputs.historical_bucket_volumes, inputs.lot_size, inputs.profile_id);
if (topicId === "D13-F01-A03") return adaptiveVwapExecution(inputs.total_quantity, inputs.realized_market_volumes, inputs.remaining_forecast_volumes, inputs.executed_quantity, inputs.lot_size, inputs.forecast_version);
if (topicId === "D13-F01-A04") return percentageOfVolumeExecution(inputs.total_quantity, inputs.participation_rate_bps, inputs.market_volume_buckets, inputs.lot_size, inputs.max_child_quantity);
throw new Error(`unsupported topic_id: ${topicId}`);
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.