Skip to content

fix(execution): stop VWAP emitting negative slice quantities (BUG-15) - #25

Merged
sijie-Z merged 1 commit into
mainfrom
fix/vwap-slice-quantities
Sep 13, 2026
Merged

sijie-Z merged 1 commit into
mainfrom
fix/vwap-slice-quantities

Conversation

@sijie-Z

@sijie-Z sijie-Z commented Sep 13, 2026

Copy link
Copy Markdown
Owner

P1.

slice_qty = max(100, int(order.quantity * volume_profile[i]))   # lot floor
slice_qty = (slice_qty // 100) * 100
allocated += slice_qty
plan.slices.append(ExecutionSlice(
    quantity=min(slice_qty, order.quantity - (allocated - slice_qty)),  # never bites
    ...))

The lot floor can exceed what is left on a small order, and the clamp meant to catch it compares against allocated after it has already been incremented — so it never bit.

Measured

Parent Plan before Sum
200 shares [100, 100, 0, -100, -200, -300, -400, -500, -600, -700] -2600

Any consumer executing those slices trades phantom short quantities. SmartRouter routes to VWAP when 0.15 <= quantity/ADV < 0.30, so an illiquid name with a small order reaches this path.

Parent Plan after
200 [100, 100] — sum 200
10 000 [1500, 1300, 1100, 900, 900, 900, 900, 700, 700, 1100] — sum 10 000

Fix

  • clamp against what actually remains
  • stop rather than emit a non-positive slice
  • set plan.num_slices to the number of slices actually produced, not the number requested — otherwise a 200-share order advertises 10 slices when it has 2
  • log when the order is too small to spread across lot-sized slices, instead of silently mis-allocating

Tests — 5, parametrized over quantities

Slices are positive, lot-sized, and sum exactly to the parent; a 200-share order is not split into ten slices; a plan never exceeds its parent.

Verified against the old implementation: the 200 and 500 cases and the over-split case fail there; 100, 1000 and 10 000 pass both ways, because they are large enough not to trip the floor. Recorded so the passing cases are not mistaken for coverage of the bug.

1341 passed, 1 skipped.

🤖 Generated with Claude Code

    allocated = 0
    for i in range(n_slices):
        if i == n_slices - 1:
            slice_qty = order.quantity - allocated      # remainder
        else:
            slice_qty = max(100, int(order.quantity * volume_profile[i]))
            slice_qty = (slice_qty // 100) * 100
        allocated += slice_qty
        plan.slices.append(ExecutionSlice(
            quantity=min(slice_qty, order.quantity - (allocated - slice_qty)),
            ...))

The lot floor can exceed what is left on a small order, and the clamp meant to
catch it compared against `allocated` *after* it had already been incremented,
so it never bit. A 200-share parent over 10 slices came out as

    [100, 100, 0, -100, -200, -300, -400, -500, -600, -700]

summing to -2600. Any consumer executing those slices trades phantom short
quantities. `SmartRouter` routes to VWAP when 0.15 <= quantity/ADV < 0.30, so
an illiquid name with a small order reaches it.

Fix: clamp against what actually remains, stop rather than emit a non-positive
slice, and set `plan.num_slices` to the number of slices actually produced
rather than the number requested. If the order is too small to spread across
lot-sized slices, that is logged rather than silently mis-allocated.

    before  200 shares -> [100, 100, 0, -100, ..., -700]   sum -2600
    after   200 shares -> [100, 100]                       sum   200
    after  10000 shares -> [1500, 1300, 1100, 900, 900, 900, 900, 700, 700, 1100]

Tests (5, parametrized over quantities): slices are positive, lot-sized, and
sum exactly to the parent; a 200-share order is not split into ten slices; and
a plan never exceeds its parent. Verified against the old implementation --
the 200 and 500 cases and the over-split case fail there; 100, 1000 and 10000
pass both ways, because they are large enough not to trip the floor.

1341 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 48431def-9a66-40a0-b11b-64c79c7a05d1


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sijie-Z
sijie-Z merged commit 7aed6b4 into main Sep 13, 2026
9 checks passed
@sijie-Z
sijie-Z deleted the fix/vwap-slice-quantities branch September 13, 2026 06:22
@sijie-Z
sijie-Z restored the fix/vwap-slice-quantities branch September 23, 2026 05:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant