Skip to content

Fill long stop-limit orders whose limit sits on the price grid - #721

Merged
luisleo526 merged 3 commits into
mainfrom
fix-stop-limit-grid-fill
Nov 3, 2026
Merged

luisleo526 merged 3 commits into
mainfrom
fix-stop-limit-grid-fill

Conversation

@luisleo526

@luisleo526 luisleo526 commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Fill long stop-limit orders whose limit sits on the price grid

What was wrong

A stop-limit order that has been triggered fills at the print, moved to the order's tick grid. On a 0.01 grid the tick product 9999 * 0.01 is 99.990000000000009, one unit in the last place above the decimal price 99.99. A long (buy) stop-limit whose limit is that decimal price therefore got a fill price above its own limit, and the kernel refused the fill: the order produced no trade, with no error. The same happens at 100.07 and 1.15 on a 0.01 grid.

This fixes the long stop-limit case of #715.

The change

One hunk in the stop-limit branch of source_limit_fill (src/source/pine_adapter.cpp), using the same repair the ordinary limit branch already makes. Only when the projected fill price is past the order's immutable limit, and only when the print is on the chart's price grid, the fill books the print's grid spelling, which equals the limit. Every fill the kernel already admitted is kept bit for bit. A limit that the price truly did not reach is still refused. directional_tick, the nearest-tick sites, the kernel and every pinned table are untouched.

Effects to expect

  • It adds trades that main refused. On a 44-trade ETHUSDT 15-minute strategy with a stop-limit entry (2024-01-01..2024-02-01, spot bars), main produces 41 trades and this branch produces 44. All 44 match the reference trade list on entry time, side, exit time, quantity and entry and exit prices at the printed places. The 41 trades main already had are unchanged in every field except the entry counter.
  • The registered parity population does not move. A sweep of this branch over the registered population moved 0 of 7,989 probes against each of three sweeps of main, on every grade field (outcome, tier, match percentage, trade-count delta, entry, exit and PnL error percentiles, trade counts). The trade sets of the four probes whose names mention a stop-limit are byte-identical. The population has no probe on the fixed path, so the evidence for the fix is the 44-trade comparison above and the failing-then-passing rows below, not a population change. The three main sweeps ran with different codegen heads, and grades are a coarse comparison, so this shows no change in grade, not identical trades on all 7,989 probes.
  • The 1.15 sell control pins a retained booking. The short stop-limit at 1.15 on a 0.01 grid is admitted on main and books the tick product, 1.1500000000000001, one unit in the last place above the print. This branch leaves it as it is, and a control test pins it. The reference exports print prices at the instrument's decimals, so they cannot show whether the reference books that short at the product or at the decimal. If that half is ever changed, this control changes by design.
  • No pinned table moves. The four tables that pin booked prices (grid relower, brackets relower, HTF relower, host-view memo) pass unchanged.

Tests

  • test_tick_fractional_stop_limit (new): long limits at 99.99, 100.07 and 1.15 on a 0.01 grid, plus admitted rows that must book exactly what main books: short 1.15, short 100.01, whole prices, 0.07, 0.29, and an off-grid limit at 99.995. Each case checks its own binary64 geometry before any order runs. On main the three long rows fail: 12 failing lines, each a missing trade. On this branch it passes.
  • test_tick_grid_canonical_controls (new): ticks 0.05, 0.25 and 0.0004, where every product is admitted. It passes on main and on this branch, and pins that booked prices keep the tick product.
  • All 307 labelled private-fixture rows that register in the ctest-only profile give the same result on main and on this branch (302 passed, 4 skipped, and the same single failure on both).

🤖 Generated with Claude Code

luisleo526 and others added 2 commits November 3, 2026 03:15
…trols

A long stop-limit whose limit is a cent price (99.99, 100.07, 1.15 on a
0.01 grid) is refused by the kernel on observed ticks: the fill price is
projected to k * tick, and k * 0.01 sits one binary64 ULP above the
decimal limit, so the buy fill lands past the immutable limit.

test_tick_fractional_stop_limit pins this at tick 0.01: three long rows
(red on main, trades=0) and the admitted rows that must stay as main
books them (short 1.15 keeps the product 1.1500000000000001, short
100.01, whole prices, 0.07, 0.29, and an off-grid limit 99.995 that the
product already admits). Each case checks its own binary64 geometry
before any order runs.

test_tick_grid_canonical_controls pins ticks 0.05, 0.25 and 0.0004,
where every product is admitted: booked prices keep the product, and
both files are green on main for those rows.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
In the stop-limit branch of source_limit_fill, the projected fill
(directional_tick of the print) can sit one binary64 ULP past the
immutable limit, which the kernel refuses. Reuse the repair the ordinary
limit branch already makes: only when the projection is past the limit,
and only for a print on the chart grid, book that print's grid spelling.
An admitted projection is kept bit for bit, so booked prices elsewhere
(including every short row and every product the kernel admits) do not
change. directional_tick, nearest_tick, the kernel and every pin table
are untouched.

Not changed by design: a short 1.15 stop-limit still books the product
1.1500000000000001; the hunk fires only where main's value is refused.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.

Not checked: tests were not run; formal verification proved 0 of 1 changed function(s) (1 not verified).

Formal verification. PR-changed functions: 0/1 verified (0 proven, 0 may-equivalent, 0 distinguished) · 1 not verified (1 unsupported).

Not verified on this run: resolve\_source\_terms (unsupported).


Graphify review — findings

Fixes stop-limit entries on observed ticks that silently never filled when the tick-grid projection of the print landed one binary64 ULP past the order's immutable limit (e.g. a buy limit at 99.99 on a 0.01 grid projected to 99.990000000000009), which the kernel rejected as invalid terms with no error. In resolve_source_terms, if the projected fill price is outside the limit and the print is on the chart grid, the fill now books that print's grid spelling instead. Projections the kernel already admits are kept bit for bit, so existing fills don't change. Adds test_tick_fractional_stop_limit, which covers the refused rows (99.99, 100.07, 1.15) plus the admitted controls, and registers it and test_tick_grid_canonical_controls in the source-layer build.

Review partial — this diff was larger than one review pass covers, so later files were not reviewed; some findings may be missing.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 701 functions depend on the 615 functions this change touches.

Health — grade B; 10 existing hotspot(s) in the area this change touches (pre-existing, not introduced here):

  • consume_matched_request — 2 callers, 43 callees (high)
  • source_trigger_threshold() — 12 callers, 7 callees (high)
  • match_path — 2 callers, 35 callees (high)
  • execute_current — 2 callers, 24 callees (high)
  • invoke_callback — 4 callers, 12 callees (high)
  • pump_batch — 4 callers, 12 callees (high)
  • deliver_confirmed_script — 2 callers, 23 callees (high)
  • directional_tick() — 10 callers, 4 callees (high)
  • …and 2 more

Verification — 701 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 653 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify resolve\_source\_terms.

The verifier did not have enough to check resolve\_source\_terms, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: unsupported: member function — receiver/self (`this`) not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver

The comparison-side fix adds a net 16 lines to src/source/pine_adapter.cpp, which moves every line the published pages cite after it. This commit re-anchors those citations only: the citation digits in docs/adr/0001-kernel-adapter-boundary.md, docs/design/arena-bound.md, docs/design/native-feature-parity.md and docs/native-refactor-progress.md, moved by the repository's own tools (check_doc_anchors.py --fix and reanchor_doc_citations.py against main) and, for the three citations those tools left as ambiguous or comment-only, by the same sixteen lines after checking that the cited source line is byte-identical on main and on this tree. No prose, symbol name or source file changes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@luisleo526
luisleo526 merged commit 8412031 into main Nov 3, 2026
26 of 32 checks passed
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