When to Use
Use this skill when a strategy has an end-to-end tick-to-trade budget and you need to decide how to split it across pipeline stages, whether the split is feasible, and where optimisation effort will actually pay. It is the skill the other latency skills point at when they need a stage budget derived rather than assumed.
The budget itself comes from the opportunity, not from the hardware. Aquilina, Budish and O'Neill's message-level study of FTSE 100 order books found the modal latency-arbitrage race is decided in 5–10 microseconds. A strategy competing in those races has a budget in single-digit microseconds; a strategy capturing a slower signal may have milliseconds. Nothing in this module tells you which you are — it audits the split of a total you supply.
Three things this module exists to get right:
- The total budget is not the sum of the stage budgets. They are independent numbers, and both directions of mismatch matter. An allocation summing above the total means a trace can breach end-to-end with every stage inside its own share — the code is fine and the budget is wrong. An allocation summing below the total leaves headroom no stage owns.
- "Bottleneck" needs one definition, not two. This module always names the stage furthest over its allocated share, and when nothing is over, the stage with the least remaining headroom in microseconds. Ranking by raw duration instead just names whichever stage was given the largest budget.
- Per-stage P99s do not add up to the total P99, and the error is not conservative. See
profile_batchbelow and the Pitfalls.
When NOT to Use
- As the instrumentation. This module reads no clock and times nothing. It audits stage durations captured elsewhere, off the hot path. Capturing them belongs to
tick-to-trade-latency-measurementandnetwork-interface-level-tick-timestamping. - For in-host nanosecond trace accounting.
colocation-latency-budget-accountingis the NIC-ingress-to-NIC-egress skill: integer nanoseconds, its own phase model, trace quarantine on non-monotonic timestamps. This skill works in microseconds at strategy-stage granularity and is about the allocation, not the trace. Use that one for hot-path forensics, this one for budget design. - For general percentile/SLA machinery. If the question is "does this one latency series meet its SLA",
latency-monitoring-percentile-based-slasadds coordinated-omission correction, P99.9 and fleet pooling. This skill computes only the P99 it needs to audit an allocation. - With the shipped budgets unchanged.
2 / 3 / 10 / 5 / 5µs is an illustrative split of 25 µs so the module runs out of the box. No regulator, exchange, vendor or standards body publishes per-stage tick-to-trade budgets — seereferences/standards.md. Derive yours from the opportunity your strategy is actually trading. - As a live circuit breaker. This is an out-of-band audit over captured traces. Halting on a latency excursion belongs in
risk-control-latency-budgetandkill-switch-and-drawdown-circuit-breakers. - As a regulatory timestamping record. MiFID II RTS 25 governs business clock accuracy against UTC for reportable events; it is not a latency SLA, and the relative durations this module consumes are not a compliant timestamping record.
Prerequisites
- A stage duration in microseconds for every one of the five stages of each trace:
INGRESS_NETWORK,MARKET_DATA_DECODE,SIGNAL_COMPUTATION,PRE_TRADE_RISK,EGRESS_ORDER_ENCODE. An incomplete trace is rejected, not zero-filled. - Durations from one clock domain. NIC hardware timestamps and
CLOCK_MONOTONICare different clocks; subtracting across them yields an offset, not a duration. Convert before building a trace —hardware-timestamping-vs-software-timestamping-accuracyandclock-synchronization-ptp-for-trading-hostscover the conversion. - A timer that can resolve the stages you are budgeting. Under RTS 25 an HFT firm's business clock need only be accurate to 100 µs with 1 µs granularity. That clock cannot measure a 2 µs stage at all; per-stage timing needs an in-host monotonic counter or NIC hardware timestamps, and a stage reported as
0.0µs means the timer could not resolve it, not that the stage was free. - An end-to-end
total_budget_usderived from the opportunity, and a per-stage allocation of it. - At least 100 traces before a batch P99 means anything.
Units
Every latency, budget and percentile in this module is microseconds (µs) as a Python float. colocation-latency-budget-accounting, the nearest sibling, is in integer nanoseconds — a value moved between the two without conversion is off by 1,000× and neither module will catch it. Reported fields are rounded to 3 dp (1 ns); every budget comparison runs on the unrounded value, so a 25.000001 µs total against a 25 µs budget is a breach and not a 25.0 µs pass.
Workflow
- Set the total budget first, then allocate it. Construct
StrategyLatencyBudgetDecompositionEngine(stage_sla_budgets, total_budget_us). Every stage must appear instage_sla_budgets— a partial map raises rather than inventing a budget for the stages you left out. Omittingtotal_budget_usdefaults it to the sum of the stage budgets, i.e. an allocation with no headroom. If the allocation sums above the total the engine logs a warning and setsunallocated_budget_usnegative: that configuration is legal, but it means a breach can occur with no stage at fault, and the fix is re-allocation rather than optimisation. - Reject unusable traces rather than repairing them.
decompose_tick_to_traderaises on a missing stage, an unknown stage key, and on NaN, Inf, negative, boolean or non-numeric durations. Each of these otherwise yields a passing report: a missing stage reads as zero latency, a NaN breaches nothing while making the total NaN, and a negative stage subtracts from the total and can offset a real breach into an apparent pass. Catch the error per trace, count the quarantine, and continue — a quarantine rate that is silently dropped biases the tail downwards, because instrumentation defects correlate with the slow path. - Read the single-trace verdict against the right denominator.
is_within_budgetcompares the total tototal_budget_us(strict>for a breach, so landing exactly on budget is inside).breached_stageslists stages over their own share. These are independent: either can fire without the other. - Take
primary_bottleneck_stageas "furthest over its share". Always defined, breach or not; negativeexcess_uson the named stage means nothing was over and this was merely the tightest. Ties break towards the earliest stage in hot-path order. If you want the ratio view instead of the microsecond view, eachStageLatencyMeasurementexposesbudget_utilization. - Check what a stage can arithmetically achieve before optimising it. On a breach,
stage_reduction_required_fraction[stage]isdeficit / stage_latency— the fraction of that stage you would have to remove to get back inside budget. A value above 1.0 means that stage cannot close the gap even if deleted entirely (Amdahl's bound: the most one stage can remove from the total is its own duration). Engineers routinely attack the stage that feels slow rather than the one capable of closing the deficit; this is the number that settles it. - Profile the tail over a batch, never from one trace.
profile_batch(reports)returns nearest-rank P50/P95/P99 of the per-trace totals plus per-stage P99s. Readp99_total_us. Do not sumstage_p99_us.comonotonic_gap_usis the signed difference between the sum and the measurement, and it is a diagnostic rather than a bound:- ≈ 0 — the stages spike together. One shared cause (GC, scheduler preemption, IRQ storm), not five.
- > 0 — the stages stall independently and each stall is common enough for its own stage P99 to see it; summing double-counts excursions that rarely coincide.
- < 0 — the dangerous case. Stalls are spread thinly enough across stages that no single stage's P99 resolves them, while the totals do. Stage-by-stage budgeting would approve a pipeline already over its end-to-end P99.
- Do not read an approval off a batch too small to resolve P99. A breach is reported at any sample count — an over-budget trace was genuinely observed. An approval requires at least 100 traces; below that the status is
LATENCY_BUDGET_INSUFFICIENT_SAMPLES, because the nearest rank for P99 lands on the batch maximum and the "P99" is just the worst trace seen.
Full procedure: see
references/workflows.md. Standards, sources, and what nobody publishes: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Adding up per-stage P99s to get an end-to-end P99. This is exact only when the stages are comonotonic — when they spike together. When they stall independently it is an under-estimate, because the total sees the union of the stages' stall events while each stage's P99 sees only its own. Five stages each stalling on a different 1% of 100 traces put 5% of totals into the tail: the measured total P99 is 19.0 µs while the stage P99s sum to 11.0 µs (worked in the tests). This is the quantile functional failing to be subadditive over independent skewed positions, the same structure as the standard defaultable-bond counterexample for Value-at-Risk. Measure the total.
- Defining the total budget as the sum of the stage budgets. It makes over-commitment unrepresentable, so the one configuration that produces breaches with no stage at fault is the one you cannot see.
- Ranking stages by raw duration and calling the biggest the bottleneck. It names whichever stage was allocated the most time. A signal stage at 7 µs of a 10 µs share has 3 µs spare while an ingress stage at 1.5 µs of a 2 µs share has 0.5 µs — the second is the one about to spend someone else's budget.
- Optimising a stage that cannot close the deficit. A stage that is 4% of the total cannot recover a 20% overshoot even if it is deleted. Check
stage_reduction_required_fractionbefore assigning the work. - Treating a missing stage measurement as a fast stage. A dropped instrumentation point defaults to zero in any implementation that uses
dict.get(stage, 0.0), and an incomplete trace then passes an end-to-end budget it never satisfied. - Letting a NaN into a stage duration. NaN compares
Falseagainst every bound: no stage breaches, and the report is emitted with a NaN total that renders on a dashboard as a blank rather than an alarm. - Filtering out negative stage durations and auditing the rest. A negative duration proves the two timestamps came from different clock domains or ran backwards. Every other stage in that trace was bracketed by the same clocks and is wrong by an unknown amount. The trace is unusable, not partially usable.
- Timing the stages with the clock that satisfies RTS 25. A business clock permitted 100 µs of divergence from UTC and 1 µs of granularity cannot resolve a per-stage microsecond budget. RTS 25 compliance and stage-level measurement are separate problems solved by separate clocks.
- Forgetting that instrumentation is inside the budget. Every stage boundary adds a timestamp read to the path being measured, and the reads land in the hot path whose latency is under audit. Budget them, or measure with hardware timestamps that do not.
- Reading a mean and calling the path budgeted. A pipeline that is 12 µs on 999 ticks and 4 ms on the thousandth averages out to something reassuring while losing the one trade that mattered.
- Reusing millisecond intuitions for a race-speed strategy. A margin of 5–10 µs decides a latency-arbitrage race. Against that, a budget with millisecond granularity is not a tight budget; it is no budget at all.
- Comparing reports audited under different allocations.
profile_batchrejects a mixed batch: excess figures computed against different budgets are not comparable, and the pooled bottleneck would be meaningless.
Verification
- Allocation arithmetic: default budgets $\implies$
allocated_budget_us == 25.0,unallocated_budget_us == 0.0.total_budget_us=20.0$\implies$unallocated_budget_us == -5.0andis_overcommittedTruewith a logged warning. - Independent totals: with the allocation summing to 25 µs but
total_budget_us=15.0, a trace of 1.5/2.0/7.0/3.5/4.0 µs (total 18.0) $\implies$is_within_budgetFalse,breached_stages == [],budget_deficit_us == 3.0— a breach with no stage at fault. - Bottleneck definition: on that same compliant trace the per-stage headroom is −0.5/−1.0/−3.0/−1.5/−1.0 µs $\implies$
primary_bottleneck_stageisINGRESS_NETWORK, notSIGNAL_COMPUTATION(which v1.0.0 named, having ranked by raw duration). A tie at +1.0 µs between ingress and decode $\implies$ ingress, the earlier stage. - Boundary: every stage exactly on its share $\implies$
is_within_budgetTrueandbreached_stages == []. Ingress at 2.000001 µs $\implies$ breach, whiletotal_tick_to_trade_latency_usstill displays 25.0 — confirming comparison precedes rounding. - Amdahl bound: the 18.0 µs trace against a 10.0 µs budget $\implies$
budget_deficit_us == 8.0andstage_reduction_required_fraction[SIGNAL_COMPUTATION] == 8/7 > 1.0, with every stage above 1.0 $\implies$ no single stage can close this deficit. - Percentile arithmetic against hand-derived ranks: over totals 1..100, nearest rank returns P50 = 50, P95 = 95, P99 = 99 $\implies$ every figure is an observed total. 50 samples at 1 µs plus 50 at 9 µs $\implies$ P50 = 1.0, never an interpolated 5.0.
- Non-additivity of stage P99s: 100 traces of 1/1/5/2/2 µs where five traces each add 8.0 µs to a different stage $\implies$
p99_total_us == 19.0,sum_of_stage_p99_us == 11.0,comonotonic_gap_us == -8.0. The same 100 traces with a single trace stalling in every stage at once $\implies$ gap 0.0. - Sample sufficiency: 100 clean traces $\implies$
is_p99_resolvableTrue, statusLATENCY_BUDGET_HEALTHY; 99 $\implies$FalseandLATENCY_BUDGET_INSUFFICIENT_SAMPLES; 10 traces containing one 33 µs breach $\implies$LATENCY_BUDGET_BREACH(a breach needs no resolution guarantee). - Trace rejection: a trace missing any stage, or carrying a NaN, Inf, negative, boolean, non-numeric or implausibly large duration, or an unrecognised stage key, each raises rather than producing a report. All errors subclass
ValueError. - Config rejection: a partial
stage_sla_budgetsmap (v1.0.0 gave the omitted stages an invented 10 µs each, turning a 2 µs budget into 42 µs), a zero/negative/non-finite stage budget, and a non-positivetotal_budget_useach raiseLatencyBudgetConfigError. Mutating one engine'ssla_budgetsleaves the next engine's defaults at 2.0 µs. - Run
python -m unittest discover -s skills/strategy-latency-budget-decomposition/scripts.
Related Skills
tick-to-trade-latency-measurementcolocation-latency-budget-accountinglatency-monitoring-percentile-based-slasmodel-inference-latency-budget-for-live-tradingnetwork-jitter-impact-on-strategy-performancerisk-control-latency-budgethardware-timestamping-vs-software-timestamping-accuracyclock-synchronization-ptp-for-trading-hostsnetwork-interface-level-tick-timestampingco-location-provider-selection-and-network-topology