fix: make the quantity-zero tolerance scale-relative - #67
Draft
brentianpalmer wants to merge 1 commit into
Draft
Conversation
The engine decides a position is closed when the post-fill quantity is
under an absolute 1e-12. float64 spacing grows with magnitude, so that
threshold is only correct over one binade: one ULP is 9.09e-13 just below
8192 units and 1.82e-12 from 8192 upward. At and above 2**13 units an
exact close that lands one ULP off zero is no longer recognised, the
position key survives in broker.positions, and the engine reports an open
position holding ~1e-12 shares. Since n_positions is len(broker.positions),
the reported count is one too high until the name is traded again.
Observed on a short cover of 10,927.322882 shares: the residual was
exactly 2**-39, one ULP of that binade, and the count stayed high for 20
sessions.
Replace the absolute epsilon with quantity_zero_tolerance() in
core/shared.py, which scales with the operands that produced the residual:
tol = max(1e-12, 16 * ulp(max(|old_qty|, |signed_qty|)))
The scale is the operands, not the result -- the result is ~0 by
construction, so a tolerance taken against it could never fire. The floor
keeps the previous absolute behaviour everywhere float64 spacing is finer
than it, which is every magnitude below 8192; a call passing a single
operand is therefore exactly equivalent to the old rule. A larger fixed
epsilon is not the repair: it moves the failure point instead of removing
it, and erases genuine small positions. For the same reason the tolerance
carries no upper cap, which would be scale-blind in turn.
Every quantity-zero comparison in the package now routes through the one
primitive: position closure in the fill executor, the submission-precheck
shadow book, the shadow-queue validation and commit paths, and the
gatekeeper's position normalisation. The sites previously disagreed at the
threshold itself, mixing `<` and `<=`; they are unified on the `<=` form
the majority already used, so one predicate now holds everywhere.
OrderBook._MIN_ORDER_SIZE is deliberately unchanged. It is an economic
policy -- the smallest order the engine accepts -- not a statement about
floating-point residue.
Adds tests/test_quantity_zero_tolerance.py (66 tests) covering the binade
boundary, ULP-graded residues on both sides of the bound, genuine small
long and short positions in the same magnitude range, partial fills,
repeated fills, position-key/count consistency, cross-site agreement,
sign symmetry, subnormal and non-finite inputs, and the sites left
unchanged. Expected boundaries are reconstructed from frexp rather than
from the production helper.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #66.
Base:
877332b04aa00691e372d5bd6ef251104f45c204(tagv0.1.0b21). Draft because the base is the released tag rather than currentmain; happy to rebase ontomain(883785e6) on request — no commit between the two touches any of the changed files.The defect
FillExecutor._update_positionzeroes a post-fill quantity against an absolute1e-12. float64 spacing grows with magnitude, so that threshold only works over one binade: one ULP is 9.09e-13 just below 8192 units and 1.82e-12 from 8192 up. At and above 2^13 an exact close that lands one ULP off zero is not recognised, the key survives inbroker.positions, andn_positions(a rawlen()of that dict,engine.py:275-277) reports a position holding ~1e-12 units.Observed on a short cover of 10,927.322882 units: residual exactly
2**-39, count high for 20 sessions. Issue #66 has a standalone reproducer.The rule
core/shared.pygains one primitive next to the existingCASH_TOLERANCE:Callers pass the quantities that produced the residual, never the residual itself — it is ~0 by construction and carries no scale, so a tolerance derived from it could never fire.
The floor keeps the previous absolute behaviour everywhere spacing is finer than
1e-12, which is every magnitude below 8192. Since16 * ulp(x) < xfor every normalx, a call with a single operand is exactly equivalent to the old rule — the "is this book quantity zero?" sites keep their behaviour, and only the closure sites, which pass both operands, become scale-aware.Rationale for 16 ULP, the absent cap, subnormal/non-finite handling, and residue-versus-genuine-position separation is in
docs/concepts/quantity-zero-tolerance.md.Site dispositions
All 19 quantity-zero comparisons in the package are migrated onto the primitive; the two constants they used (
FillExecutor._qty_zero_epsilon,OrderBook._QTY_EPS) are removed.execution/fill_executor.py(old_qty, ctx.signed_qty)— the closure sitecore/order_book.py(old_qty, size)on the closure/precheck paths, single operand on the book-is-empty filterscore/execution_engine.py(current_qty, qty_delta)on the shadow-queue closure paths, single operand elsewhereaccounting/gatekeeper.pyLeft unchanged:
OrderBook._MIN_ORDER_SIZE(1e-8). It is an economic policy — the smallest order the engine accepts — not a statement about floating-point residue. A regression test pins it.One deliberate consistency change. The sites previously mixed
<(fill_executor,_simulate_position_update,gatekeeper) and<=(the shadow-book and shadow-queue paths), so they disagreed exactly at the threshold. They are unified on<=, the form the majority already used, so one predicate —|q| <= tolmeans zero — now holds everywhere. This was caught by the cross-site consistency test, not assumed.Tests
tests/test_quantity_zero_tolerance.py, 66 tests: the binade boundary, ULP-graded residues on both sides of the bound, exact closes across several powers of two, genuine small long and short positions in the same numerical magnitude range, partial reductions and covers, repeated fills ending flat and not, position-key/count consistency, cross-site agreement between the executor and both shadow paths, long/short symmetry, zero scale, subnormals, non-finite inputs, and the retained sites.Expected boundaries are reconstructed from
math.frexprather than obtained from the production helper, so a test cannot agree with a wrong rule.Verification
877332b0pytest tests/ruff check --no-fix src/ tests/ruff format --check src/ tests/ty checkuv buildThe suite delta is exactly the 66 new tests. Lint and typecheck counts are unchanged; the pre-existing findings are in files this branch does not touch.
Note for anyone running lint locally: this repo sets
fix = trueunder[tool.ruff], so a bareruff checkrewrites files. The numbers above use--no-fix.uv sync --devcould not be used on the development machine — it fails buildingllvmlite0.46.0 from source (spawn() got an unexpected keyword argument 'dry_run'), reproduced identically at the base commit and unrelated to this change. Environments were built withvenv+pipinstead.