Build a deterministic equal-time schedule that conserves every lot and reaches the parent target exactly.
The decision this tutorial makes visible
Time-weighted average price execution provides a transparent baseline when time, rather than expected volume, determines pacing.
The precise question is: How should a lot-aligned parent quantity be distributed across equal time buckets?
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
TWAP is a metronome: each equal slice of time receives nearly the same number of executable lots.
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 divides a fixed horizon into equal temporal buckets and allocates parent lots as evenly as possible using deterministic largest-remainder assignment.
| Variant | Definition | Best use | Main limitation |
|---|---|---|---|
| Equal-time canonical TWAP | Equal time and near-equal quantity | Transparent baseline | Ignores liquidity shape |
| Randomized TWAP | Jitter child timing/size around schedule | Reduce regularity | No longer deterministic |
| Price-aware scheduler | Change aggression using prices | Tactical execution | Different algorithm |
What is sourced, selected, synthetic, and derived
| Role | Material claim | Evidence | Boundary |
|---|---|---|---|
| Sourced fact | The SEC staff report identifies TWAP as a common execution-algorithm objective, while FIXatdl defines machine-readable strategy parameters and validation rather than a universal TWAP schedule. | S1, S2 | Neither source prescribes earliest-bucket residual assignment, integer-millisecond boundaries, fill behavior, or performance for the repository's synthetic parent. |
| Implementation choice | The canonical algorithm divides a fixed horizon into equal temporal buckets and allocates parent lots as evenly as possible using deterministic largest-remainder assignment. | 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 | A synthetic 12,000-unit parent over 24 buckets with 100-unit lots produces 24 targets of 500 units and zero residual. | 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
q_i ≈ Q/N, with Σq_i = Q and q_i mod lot = 0
| Symbol | Meaning | Unit | Policy |
|---|---|---|---|
| Q | parent quantity | units | positive and lot-aligned |
| N | equal time buckets | count | positive |
| ℓ | lot size | units | positive |
| q_i | bucket target | units | integer lots |
- Use integer milliseconds and integer lots.
- Time boundaries use proportional integer division; the final boundary equals end_time_ms.
- Quantity equality is exact, not tolerance-based.
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, horizon, and bucket count
- Convert parent to lots
- Assign equal base lots
- Distribute residual lots by stable bucket order
- Build exact time boundaries and cumulative targets
Production-minded operational checklist
- Freeze parent quantity and benchmark horizon
- Validate trading calendar/session separately
- Choose lot and bucket count
- Generate conserved targets
- Hand targets to a child-order tactic with fill feedback
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 24 contiguous one-minute buckets of 500 units, ending at a cumulative target of 12,000 with zero residual. The complete input and output
are in datasets/canonical-input.json and datasets/expected-output.json.
A synthetic 12,000-unit parent over 24 buckets with 100-unit lots produces 24 targets of 500 units and zero residual.
Counterfactual checkpoint
One lot per bucket. Set the parent to exactly 60 lots and the horizon to 60 buckets. The output changes because The parent contains just enough executable lots to keep every declared bucket nonempty.
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 parent-size sweep | Step 0 · canonical fixture | Increase the parent in single lots while preserving 24 equal time buckets. Synthetic data; schedule output is not a fill guarantee. | complete-schedule | first bucket 500 units | 24 buckets; 0 residual | 1 |
| Exact equal-lot allocation | Step 30 · comparison focus | Use a parent that divides evenly across all 24 buckets with no residual lots. Synthetic data; schedule output is not a fill guarantee. | complete-schedule | first bucket 500 units | 24 buckets; 0 residual | 1 |
| Fewer-bucket comparison | Step 30 · comparison focus | Use 20 equal-time buckets so each target carries more lots. Synthetic data; schedule output is not a fill guarantee. | complete-schedule | first bucket 800 units | 20 buckets; 0 residual | 1 |
| More-bucket comparison | Step 30 · comparison focus | Use 30 equal-time buckets so each target carries fewer lots. Synthetic data; schedule output is not a fill guarantee. | complete-schedule | first bucket 500 units | 30 buckets; 0 residual | 1 |
| Coarse 12-bucket schedule | Step 30 · comparison focus | Compare a lower-frequency equal-time schedule over the same teaching horizon. Synthetic data; schedule output is not a fill guarantee. | complete-schedule | first bucket 1300 units | 12 buckets; 0 residual | 1 |
| Fine 36-bucket schedule | Step 30 · comparison focus | Compare a higher-frequency equal-time schedule without introducing volume weights. Synthetic data; schedule output is not a fill guarantee. | complete-schedule | first bucket 500 units | 36 buckets; 0 residual | 1 |
| One-lot-per-bucket boundary | Step 30 · comparison focus | Set parent lots exactly equal to bucket count; every bucket receives one lot. Synthetic data; schedule output is not a fill guarantee. | complete-schedule | first bucket 100 units | 60 buckets; 0 residual | 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:
- parent lots ≥ buckets — Build schedule, because At least one lot can reach each bucket.
- residual lots remain — Assign by stable order, because Exact conservation.
- horizon invalid — Reject, because Time partition impossible.
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:
- Every target is lot-aligned.
- Scheduled quantity equals the parent exactly.
- Bucket time intervals are contiguous and cover the full horizon.
- 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 FIXatdl 1.2 RC1, SEC Algorithmic Trading Report. 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 construct deterministic equal-time parent schedules. The learning flow is: Parent and child order concepts → TWAP Execution → Historical 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.
TWAP Execution calculation flow
This flow identifies the selected calculation stages and the structured output.
Takeaway: Equal time drives the schedule; lot rounding must never lose or invent parent quantity.
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 — 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.
S2 — Staff Report on Algorithmic Trading in U.S. Capital Markets
- Organization or authors: Staff of the U.S. Securities and Exchange Commission
- Source type: Official regulator staff report
- Publication or effective date: 2020-08-05
- Version: Staff report
- URL or DOI: https://www.sec.gov/file/Algo_Trading_Report_2020.pdf
- Accessed: 2026-07-30
- Jurisdiction: United States capital markets
- Supports: The use of algorithmic execution to seek benchmarks such as VWAP and TWAP and to manage trading relative to market conditions.
- Limitations: Does not prescribe the repository formulas or validate any synthetic schedule.
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.