Skip to content

Fill a stop-limit entry at the closing tick when process_orders_on_close is on - #725

Merged
luisleo526 merged 4 commits into
mainfrom
train/engine-1.10.0-r3
Nov 3, 2026
Merged

luisleo526 merged 4 commits into
mainfrom
train/engine-1.10.0-r3

Conversation

@luisleo526

@luisleo526 luisleo526 commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #707.

What

With process_orders_on_close = true, a stop-limit entry whose stop is reached by the closing quote did not fill on that close even when its limit was also marketable there. The stop-limit was only activated at a later evaluation point, so it filled on the next bar (or later after a gap), while a market, a limit-only and a stop-only entry at the same close filled on the close.

This change fills such a stop-limit entry on the closing tick. The kernel gains activate_current_stop_limit: it activates only the named pending stop-limit when the current quote reaches its stop, returns nothing when it activated or was already live, and on a refusal changes nothing. It does not execute the limit and does not evaluate any other request. The adapter calls it for the stop-limit entry at the confirmed close, and the matching check treats that activating step as a current-execution step, so the limit is tried at the same close. Market, limit-only and stop-only entries take the same path as before.

Commits

  1. Tests: a stop-limit entry with process_orders_on_close at the closing tick, next to its market, limit-only and stop-only controls; the kernel operation; one existing transcript test adjusted for the same change.
  2. The fix (src/native_order.cpp, src/source/pine_adapter.cpp, the native consumer).
  3. Source-guard follow-up: one trigger step shared by the path matcher and the current activation (behaviour unchanged), the coverage row for the new native member (it has no C spelling: its one caller is the Pine host), and the regenerated source-site inventory.
  4. Documentation anchors re-pinned with the repository's own tools (documentation only, 244 lines changed on each side, none added or removed).

Evidence

  • Full lab verify on this exact head (all eight steps, absent private fixtures): preflight, release (1502 tests), debug (1397), sanitizers (1368), kernel (729), docs, parity, and the 54-probe subset all pass.
  • Parity sweep over the 134-case population, paired with the current codegen main: the gate verdict is PASS (no-improve-no-regression): 1009 of 1009 evaluated cases comparable, no regression, and no probe moves.
  • Native-live legs on this head: live-tsan and live-sanitizers pass 120 of 120 rows each (the same totals as main). Native shards 1 to 4: still running when this pull request was opened; their result is added below as a comment.

Classification of the new reviewed site

The one new assignment site on held in NativeExecutionConsumer::activate_current_stop_limit is a std::shared_ptr<const LiveRequest>: one request row per call, with no growth in bars, trades or events. It is recorded in the reviewed site inventory (3731 sites, was 3730).

TradingView evidence

A TradingView recording of the stop-limit script from the issue (with process_orders_on_close = true) was joined to native runs, matching trades by entry time and order id.

  • Run without a quantity step (the script declares none): with this change, the stop-limit entries equal TradingView's on all 28 entries that both sides have (entry bar and exit). On current main and on v1.9.1, none of them joins: every stop-limit entry fills one bar late.
  • TradingView's printed quantities on this recording are all whole multiples of 0.00001. Running the harness with an explicit quantity step of 0.00001 reproduces that, and with this change the result then equals the recording on all 29 trades, for the stop-limit script and for its limit-only control. v1.9.1 was not re-run with that step.
  • In the run without the step, TradingView's trade 22 (2024-02-13 18:00 UTC) has no native counterpart: the engine opens no position there, and the sizes of the 7 later trades differ by about 0.56% as a consequence. This is the same on every tree and for the limit-only control, so it is not part of this change.

🤖 Generated with Claude Code

luisleo526 and others added 4 commits November 3, 2026 12:18
…sing tick

With process_orders_on_close = true, a stop-limit entry whose limit and
stop both equal the bar's close does not fill on the closing tick, while a
market, a limit-only and a stop-only entry at the same close do (the
stop-limit fills on the next bar, or later after a gap).

test_pooc_stop_limit_entry_at_close covers the stop-limit entry at the
close next to its market, limit-only and stop-only controls.
test_native_current_activation covers the kernel operation the fix adds,
activate_current_stop_limit. test_native_dormant_book_transcript is
adjusted for the same change.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ose is on

With process_orders_on_close = true, a stop-limit entry whose stop is
reached by the closing quote did not fill at that close when its limit is
also marketable there, because the stop-limit was only activated at a later
evaluation point.

The kernel gains activate_current_stop_limit: it activates only the named
pending stop-limit when the current quote reaches its stop, returns nothing
when it activated or was already live, and on a refusal changes nothing; it
does not execute the limit or evaluate any other request. The adapter uses
it for the stop-limit entry at the confirmed close, and the matching check
treats that activating step as a current-execution step, so the limit is
tried at the same close. Market, limit-only and stop-only entries take the
same path as before.

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

The native source guards refused the stop-limit activation added for the closing
tick: the native-versions guard requires exactly two code occurrences of
activation_grid(*spec) (the trail observer and the single hand-off of a trigger
transition to the core), and the head had three (the new stop-limit activation
was the third); the C-surface guard wanted a record for the new native member;
and the new assignment site had to be in the reviewed site inventory.

trigger_step() now holds the single hand-off of a trigger transition to the
request core with the run's activation grid, in the current mutation mode. The
path matcher and activate_current_stop_limit both call it; the trail observer
keeps its own. The matcher passes the same arguments in the same order inside
the same try block as before. The current activation used to call the grant-free
apply_trigger / prepare_trigger forms, which forward to the grant forms with
open_grant false, and now passes open_grant false to the same grant forms.
Behaviour is unchanged.

native_c_api.h gets a COVERAGE row for activate_current_stop_limit saying why it
has no C spelling (its one caller is the Pine host; a C host's stop-limit
activates where the matcher reaches its stop; strategy_native_execute_current_v1
takes a market request only). ci/cr150-source-sites.json is regenerated with the
repository's own --emit-site-inventory for this tree: it holds the new record
plus the order the emitter now produces (the previous order had drifted).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The stop-limit fix moves lines in the sources that the design pages cite by
file:line. The repository's own tools (scripts/check_doc_anchors.py --fix and
scripts/reanchor_doc_citations.py --ref main) move the citations; nothing is
edited by hand and no code changes.

Before: 310 bad anchors. After: all 2344 anchors resolve to their symbol.
Eight documentation files, 244 lines changed on each side (no line is added
or removed).

@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; no formal proof of the changed code.


Graphify review — findings

Regenerates the CR150 source-site manifest so its entries follow the current source ordering. This moves the reader, provider, reader_ and IdTableReadCache allocation entries and the fold_cohort_exposure/verify_cohort_exposure assignment entries in pine_adapter.cpp. It also registers a new held = ... assignment site in NativeExecutionConsumer::activate_current_stop_limit.

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 — 4178 functions depend on the 3164 functions this change touches.

Health — this change adds coupling hotspots:

  • new: check_trade() — 7 callers, 3 callees
  • new: check_activated_limit_cancelled_before_touch() — 1 callers, 6 callees
  • new: check_activated_limit_without_touch() — 1 callers, 6 callees
  • new: check_activated_stop_limit_rests_as_limit() — 1 callers, 6 callees
  • new: check_close_entries() — 1 callers, 6 callees
  • new: activate_current_stop_limit — 0 callers, 20 callees
  • new: main() — 0 callers, 9 callees

Verification — 4178 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: 4103 function(s) in the blast radius were not formally verified this run

· 7 grounded finding(s) anchored inline below.

std::optional<NativeCurrentRefusal> validate_current_execution(
const BacktestEngine& engine, const NativeCurrentExecution& command) const;
public:
std::optional<NativeCurrentRefusal> activate_current_stop_limit(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — activate_current_stop_limit

fans out to 20 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

require(probe.last_run_status() == 0, "run status must be successful");
}

void check_trade(const Probe& probe, int index, int entry, double price, int exit_submission) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — check_trade()

7 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

require(!trade.open_at_end, "expected a closed trade");
}

void check_close_entries(Input mode, Entry kind, bool is_long = true, int slippage = 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — check_close_entries()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

return input;
}

void check_activated_stop_limit_rests_as_limit(Input mode, bool is_long) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — check_activated_stop_limit_rests_as_limit()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

}
}

void check_activated_limit_without_touch(Input mode) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — check_activated_limit_without_touch()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

require(touched.pending() == 0, "touched limit must leave no pending entry");
}

void check_activated_limit_cancelled_before_touch(Input mode) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — check_activated_limit_cancelled_before_touch()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

}
} // namespace

int main() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — main()

fans out to 9 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@luisleo526

Copy link
Copy Markdown
Collaborator Author

Native-live on e0c7cc5 (tree 5e5f2f3d), with private fixtures absent: live-tsan 120/120, live-sanitizers 120/120, and native shards 1-4 415 / 377 / 396 / 416 (1604 rows), all passed. Main has 120 / 120 / 414 / 376 / 396 / 416; the two extra rows are the two tests this change adds.

@luisleo526
luisleo526 merged commit d129cc0 into main Nov 3, 2026
16 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.

process_orders_on_close = true: a stop-limit entry placed at the bar close does not fill on that close

1 participant