Normalize a versioned volume curve, allocate every parent lot, and expose the exact cumulative schedule.
The decision this tutorial makes visible
Historical VWAP pacing shifts quantity toward buckets that historically carried more market volume instead of assuming equal liquidity through time.
The precise question is: How should parent lots follow a frozen historical intraday volume profile?
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
The historical curve is a set of cups of different sizes. Parent lots are poured in proportion to those cup sizes.
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 scheduler treats nonnegative historical bucket volumes as weights and uses largest-remainder lot allocation. It does not predict today's volume or guarantee the realized VWAP benchmark.
| Variant | Definition | Best use | Main limitation |
|---|---|---|---|
| Static historical VWAP | One frozen profile | Stable intraday shape | Cannot react to today's surprise |
| Day-type profile | Select curve by weekday/event/session | Conditional seasonality | Selection risk |
| Adaptive VWAP | Reforecast with realized data | Live surprise response | More stateful |
What is sourced, selected, synthetic, and derived
| Role | Material claim | Evidence | Boundary |
|---|---|---|---|
| Sourced fact | Berkowitz, Logue, and Noser motivate VWAP as a transaction-cost benchmark; Konishi studies static VWAP execution; FIXatdl standardizes strategy parameters but not the repository's allocator. | S1, S2, S3 | The package normalizes a supplied synthetic profile and uses deterministic largest-remainder allocation; it does not reproduce Konishi's optimization or claim benchmark tracking quality. |
| Implementation choice | The canonical scheduler treats nonnegative historical bucket volumes as weights and uses largest-remainder lot allocation. It does not predict today's volume or guarantee the realized VWAP benchmark. | 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 24-bucket U-shaped profile allocates all 24,000 parent units in 100-unit lots, assigning larger targets to the high-volume opening and closing buckets. | 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
w_i = V_i/ΣV; q_i = lot_allocate(Q·w_i), with Σq_i = Q
| Symbol | Meaning | Unit | Policy |
|---|---|---|---|
| V_i | historical volume in bucket i | volume | nonnegative |
| w_i | normalized historical weight | fraction | V_i/ΣV |
| Q | parent quantity | units | lot-aligned |
| q_i | bucket target | units | largest-remainder lot allocation |
- Volume weights are derived from integer inputs and displayed to 12 decimals.
- Allocation uses integer numerator remainders rather than floating ranking.
- Historical profile availability must predate the parent decision.
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, lot, profile ID, and volume array
- Sum historical volume
- Compute each bucket's ideal lot numerator
- Allocate base lots and rank fractional remainders
- Emit cumulative targets and profile lineage
Production-minded operational checklist
- Version the source sample and filters
- Exclude hindsight revisions from point-in-time use
- Normalize the chosen curve
- Conserve lots deterministically
- Monitor realized divergence without relabeling the static schedule adaptive
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,
schedule, is all 24,000 units are allocated in 100-unit lots; the opening receives 1,500 and the highest-volume closing bucket receives 2,400. The complete input and output
are in datasets/canonical-input.json and datasets/expected-output.json.
The synthetic 24-bucket U-shaped profile allocates all 24,000 parent units in 100-unit lots, assigning larger targets to the high-volume opening and closing buckets.
Counterfactual checkpoint
Scale the entire profile by three. Multiply every historical bucket volume by the same positive constant. The output changes because Only relative bucket proportions enter Vᵢ divided by total volume; uniform scale cancels.
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 U-curve perturbation | Step 0 · canonical fixture | Perturb one point of the versioned U-shaped profile and watch exact lot migration. Synthetic data; schedule output is not a fill guarantee. | complete-schedule | peak target 2400 units | bucket 24; profile SYN-24B-U-2026-07 | 1 |
| Flat-profile equality | Step 30 · comparison focus | Give every bucket equal historical volume so largest-remainder ties resolve by stable index. Synthetic data; schedule output is not a fill guarantee. | complete-schedule | peak target 1200 units | bucket 1; profile SYN-PROFILE-2 | 1 |
| Front-loaded profile | Step 30 · comparison focus | Increase early relative weights and shift more parent lots toward the opening. Synthetic data; schedule output is not a fill guarantee. | complete-schedule | peak target 2000 units | bucket 1; profile SYN-PROFILE-3 | 1 |
| Back-loaded profile | Step 30 · comparison focus | Increase late relative weights and shift more parent lots toward the close. Synthetic data; schedule output is not a fill guarantee. | complete-schedule | peak target 2500 units | bucket 24; profile SYN-PROFILE-4 | 1 |
| Uniform scale-down invariance | Step 30 · comparison focus | Scale every profile bucket down together; relative weights should remain nearly identical. Synthetic data; schedule output is not a fill guarantee. | complete-schedule | peak target 2700 units | bucket 24; profile SYN-PROFILE-5 | 1 |
| Uniform scale-up invariance | Step 30 · comparison focus | Multiply every profile bucket by three; normalized weights and schedule must remain unchanged. Synthetic data; schedule output is not a fill guarantee. | complete-schedule | peak target 2700 units | bucket 24; profile SYN-PROFILE-6 | 1 |
| Zero-volume midday block | Step 30 · comparison focus | Set four midday buckets to zero and reallocate every parent lot to eligible buckets. Synthetic data; schedule output is not a fill guarantee. | complete-schedule | peak target 3000 units | bucket 24; profile SYN-PROFILE-7 | 1 |
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:
- profile sum positive — Normalize and allocate, because Valid weighting basis.
- lot residual remains — Largest-remainder distribution, because Exact conservation.
- profile ID missing — Reject, because Schedule cannot be reproduced.
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:
- Weights sum to one within displayed rounding.
- All targets are lot-aligned.
- Target quantities sum exactly to the parent.
- The output retains
schedule, 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 Berkowitz–Logue–Noser, 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 turn versioned historical volume curves into auditable schedules. The learning flow is: TWAP Execution → Historical VWAP Execution → Adaptive VWAP Execution. Carry the result forward only with its scope, clock, state, and evidence label.
Rendered from the canonical Mermaid sources linked by this article.
Historical VWAP Execution calculation flow
This flow identifies the selected calculation stages and the structured output.
Takeaway: Historical volume, not equal time, shapes the target curve; lot reconciliation preserves the parent.
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 — The Total Cost of Transactions on the NYSE
- Organization or authors: Stephen A. Berkowitz, Dennis E. Logue, and Eugene A. Noser Jr.
- Source type: Original peer-reviewed research
- Publication or effective date: 1988-03
- Version: Journal of Finance 43(1), 97–112
- URL or DOI: https://doi.org/10.1111/j.1540-6261.1988.tb02591.x
- Accessed: 2026-07-30
- Jurisdiction: Historical NYSE research sample
- Supports: A volume-weighted average price transaction-cost measure and its motivation relative to single-price benchmarks.
- Limitations: Historical benchmark study; it does not define the repository's execution schedule or imply current-market performance.
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.