Turn realized market volume into capped, lot-aligned child releases and expose any lag caused by the child cap.
The decision this tutorial makes visible
POV responds to actual market activity rather than a fixed completion clock, which can reduce or increase completion certainty depending on realized volume.
The precise question is: How much parent quantity should be released after each observed market-volume bucket at a constant participation target?
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
POV follows the crowd at a declared distance: more observed market volume authorizes more parent release.
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 targets a constant fraction of cumulative observed external market volume, floors to lots, caps each child release, assumes released children fill for teaching, and never exceeds the parent.
| Variant | Definition | Best use | Main limitation |
|---|---|---|---|
| Canonical observed-volume POV | Constant rate with child cap | Transparent participation | Completion time uncertain |
| Uncapped POV | Release full target deficit | Pure rate tracking | Potentially abrupt children |
| Minimum/maximum POV band | Dynamic rate range | Flexible participation | Not constant-rate POV |
What is sourced, selected, synthetic, and derived
| Role | Material claim | Evidence | Boundary |
|---|---|---|---|
| Sourced fact | Guéant models POV as execution constrained by a constant participation rate; the SEC report and FIXatdl establish broader execution-algorithm and parameter-interface context. | S1, S2, S3 | The package assumes released children fill for teaching and uses a supplied eligible-volume series, lot floor, child cap, and parent ceiling. It does not implement impact optimization or prove achieved execution participation. |
| Implementation choice | The canonical scheduler targets a constant fraction of cumulative observed external market volume, floors to lots, caps each child release, assumes released children fill for teaching, and never exceeds the parent. | 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 | At a 10% target, each synthetic observed-volume bucket increases the lot-floored cumulative target. The 700-unit child cap can create temporary lag, while the parent ceiling prevents over-release. | 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_t = min(Q, lot_floor(ρ·V_t)); child_t = min(target_t-released, child_cap, remaining)
| Symbol | Meaning | Unit | Policy |
|---|---|---|---|
| ρ | target participation rate | fraction/bps | 0<ρ≤1 |
| V_t | cumulative observed external market volume | volume | point-in-time |
| R_t | cumulative released quantity | units | lot-aligned |
| C | maximum child quantity | units | lot-aligned |
- Rate is integer basis points.
- Cumulative target and child release are floored to lots.
- Equality with parent stops all later releases.
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, rate, volume basis, lot, and child cap
- Accumulate observed market volume
- Compute lot-floored cumulative target
- Release the smaller of deficit, cap, and parent residual
- Report achieved participation and completion
Production-minded operational checklist
- Define eligible market-volume sources
- Handle corrections and late prints
- Use acknowledged fills rather than assumed releases in production
- Apply child and urgency caps
- Monitor completion risk as the window ends
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,
released_quantity, is the synthetic path releases the full 10,000-unit parent without exceeding a 700-unit child cap; achieved release-to-volume ratio ends at 827.129859 bps after parent completion. The complete input and output
are in datasets/canonical-input.json and datasets/expected-output.json.
At a 10% target, each synthetic observed-volume bucket increases the lot-floored cumulative target. The 700-unit child cap can create temporary lag, while the parent ceiling prevents over-release.
Counterfactual checkpoint
Tight child cap creates participation lag. Keep a 15% target but cap every child at 200 units. The output changes because Each observed-volume increment authorizes more than the child cap can release, so deficits accumulate.
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 rate sweep | Step 30 · canonical fixture | Increase target participation while observing child-cap lag and parent completion. Synthetic data; schedule output is not a fill guarantee. | parent-complete | achieved 827.129859 bps | 10000 released; 0 remaining | 2 |
| Exact 10% participation path | Step 30 · comparison focus | Use 1,000-unit volume buckets so 10% targets and 100-unit lots align exactly. Synthetic data; schedule output is not a fill guarantee. | participating | achieved 1000.0 bps | 2400 released; 17600 remaining | 1 |
| Front-loaded market volume | Step 30 · comparison focus | Concentrate eligible volume early and expose earlier parent completion. Synthetic data; schedule output is not a fill guarantee. | parent-complete | achieved 432.037222 bps | 13000 released; 0 remaining | 2 |
| Back-loaded market volume | Step 30 · comparison focus | Concentrate eligible volume late and delay cumulative release targets. Synthetic data; schedule output is not a fill guarantee. | parent-complete | achieved 453.752182 bps | 13000 released; 0 remaining | 2 |
| Low-volume completion risk | Step 30 · comparison focus | Reduce eligible market volume and show the parent remaining at the end of the path. Synthetic data; schedule output is not a fill guarantee. | participating | achieved 992.555831 bps | 3000 released; 10000 remaining | 1 |
| High-volume parent ceiling | Step 30 · comparison focus | Triple eligible volume and show that release stops exactly at the parent. Synthetic data; schedule output is not a fill guarantee. | parent-complete | achieved 358.422939 bps | 13000 released; 0 remaining | 1 |
| Tight child-cap lag | Step 30 · comparison focus | Use a 15% target with a 200-unit child cap so target deficits accumulate. Synthetic data; schedule output is not a fill guarantee. | participating | achieved 397.022333 bps | 4800 released; 8200 remaining | 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:
- target exceeds released — Release capped deficit, because Behind participation target.
- target equals released — Release zero, because Already on target.
- parent exhausted — Stop, because Completion boundary.
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:
- Released quantity never exceeds the parent.
- Every child is lot-aligned and no larger than the cap.
- No child is released before observed volume creates a target deficit.
- The output retains
released_quantity, 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 Guéant constant participation, 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 build capped, lot-aligned realized-volume participation schedules. The learning flow is: Adaptive VWAP Execution → Percentage-of-Volume Execution → Almgren-Chriss Optimal Execution. Carry the result forward only with its scope, clock, state, and evidence label.
Rendered from the canonical Mermaid sources linked by this article.
Percentage-of-Volume Execution calculation flow
This flow identifies the selected calculation stages and the structured output.
Takeaway: POV's clock is observed market volume; child caps trade tracking precision for bounded releases.
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 — Execution and Block Trade Pricing with Optimal Constant Rate of Participation
- Organization or authors: Olivier Guéant
- Source type: Original research preprint
- Publication or effective date: 2012-10-29
- Version: arXiv:1210.7608
- URL or DOI: https://arxiv.org/abs/1210.7608
- Accessed: 2026-07-30
- Jurisdiction: General execution research
- Supports: Percentage-of-volume as constant participation relative to market volume and its distinction from other execution objectives.
- Limitations: The package does not implement the paper's impact model, optimal-rate derivation, or block-pricing result.
S2 — 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.
S3 — 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.