Skip to content

Fix the remaining reversal and exit-sizing cases (#803) - #820

Merged
luisleo526 merged 8 commits into
train/engine-1.11.1from
fix/reversal-class-cases
Nov 11, 2026
Merged

luisleo526 merged 8 commits into
train/engine-1.11.1from
fix/reversal-class-cases

Conversation

@luisleo526

@luisleo526 luisleo526 commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes the remaining reversal and exit-sizing cases reported in #803, on top of the fix already in this train (#817).

Product changes (src/source/pine_adapter.cpp, include/pineforge/source/pine_adapter.hpp):

  • Reversal exits under ANY: a reversal's exits are sized from the new entry. Exits of a same-side parent are kept.
  • Reversal after a partial close: a reversing entry placed after a partial close keeps its opening leg. Every same-bar close is summed, not just the first. The entry counts as placed after a full close only when those closes cover the opposite book, with the quantity epsilon allowed in that compare; otherwise it closes the rest of the position and opens its own quantity.
  • Same-id reversal under FIFO: exits placed against held lots of that id are cancelled. Exits bound to a cohort or to a future entry are kept.

Data: re-pins the frozen digests that the reversal exit changes move (tests/test_adapter_quiet_bar_pinned.inc, tests/test_adapter_live_state_equivalence_data.hpp). No other pins move, and the x86_64 layout probe is unchanged.

Tests: new test_adapter_exit_class_any_same_side, test_adapter_partial_close_reversal and test_reversal_same_id_exits. Their expectations are derived by arithmetic.

  • Each new case that tests a change fails on the base and passes with the fix. The rows marked as controls, and the covering rows that tell the epsilon token and the loop sum apart from the fixed code, pass on the base too.
  • The partial-close rows tell the epsilon token and an early-stop variant apart. The rows covered by closes mark the pending entry, and partial closes do not.

CHANGELOG.md has one entry per case.

🤖 Generated with Claude Code

luisleo526 and others added 8 commits November 10, 2026 21:44
…side parent's exits

Two more cases of a named strategy.exit placed with a reversing or
re-entering strategy.entry, each measured against TradingView with a fixed
quantity of 3 reversed into 2 and a 50 percent take-profit (TradingView closes
1 at the limit and 1 at the stop in every case).

With close_entries_rule = "ANY" the basis of a named exit is the whole held
position. For an exit whose entry is pending on the OTHER side of the held
position and holds no lots yet, that counted the old position (3) beside the
pending entry (2): half of 5 is 2.5, which the take-profit clipped to the whole
new position and left the stop nothing. The held book now contributes nothing
to the basis when the pending parent of the exit opens the opposite side and
the named entry holds no lots; every other case keeps the whole position.

When a strategy.close of the old position and a same-side strategy.entry of
another id share one bar, the exits of the new entry were removed when the
position went flat, because the rule that keeps an exit for a pending parent
only admitted a parent on the opposite side. The side test is dropped: a parent
placed on the same bar before the exit keeps its exits whichever side it opens.

tests/test_adapter_exit_class_any_same_side.cpp holds three rows taken from the
TradingView tapes of these cases and two guards (a reused entry id behind a
close keeps no exit, and an ANY book of two same-side entries keeps its basis).

Refs #803.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A script that closes part of its position with strategy.close(id, qty) and, on
the same bar, enters the opposite side with strategy.entry did not open the new
position: the entry was resolved as if the close had flattened the book, a plain
transaction, so it closed the rest of the old position and opened nothing.
TradingView closes the rest and opens the full new position (TradingView tape
sha256: c3e77e4e697fe85e5ba7d0e27bd1a14e47e569ed1c0a49f84cf29e68d045af1e).

place_entry now adds up the quantities of the closes placed before the entry on
the same bar and marks the entry as following a close only when they cover the
opposite book. A partial close leaves the entry a reversal, as it is without the
close. A full close, close_all, a close-first entry and every other placement
keep their behaviour; no state field is added.

tests/test_adapter_partial_close_reversal.cpp holds the rows: a partial close
then a reversing entry to each side, and the two full-close guards. The frozen
digests of the quiet-bar battery (the twelve runs that contain this shape) and of
three configurations of the live-state test move with this change; they are
re-pinned, with the tape named next to each edit, in the change that finishes
the series.

Refs #803.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…der the same id

A script that holds lots under an id, places strategy.exit orders against that
id and, on the bar of a strategy.entry that reverses the position under the
same id, kept those exits for the new position, where they went on to close it.
TradingView closes the old lots at the reversal and ends the exits that were
placed against them (TradingView tape sha256:
638438371ac6e073b61b689d01af352db6e602230f217c75336492de4e26033e).

Under the default FIFO close rule an exit placed while its entry id held lots
carries the book as its owner; an exit placed before the id held lots waits on
the id's next fill. When the entry that closes lots of its own id is applied,
the reversal now cancels the exits of the first kind for that id and keeps the
second kind, as before. A reversal with no such exit keeps the previous
selection and its scratch counters; close_entries_rule = "ANY" and every other
placement are unchanged. No state field is added.

tests/test_reversal_same_id_exits.cpp holds the rows: the reversal to each side
with the exits placed on the reversal bar, the same exits placed after a close
of the old position, the new entry under another id, and exits placed before
the reversal. The frozen digests of one scenario of the quiet-bar battery
(Random25) move with this change; they are re-pinned, with the tape named next
to each edit, in the change that finishes the series.

Refs #803.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Three changes of this series alter which exits and orders survive a
reversing entry: an exit placed with a same-side entry on the bar where the
position is closed is kept when the position goes flat, an entry placed
after a partial close now closes the rest and opens the new position, and an
exit placed while its entry id holds lots is cancelled when an entry of the
same id reverses those lots. The frozen digests of the live-state
equivalence test (10 of 108 configurations) and of the quiet-bar battery
(16 of 276 rows) move with them; no other row moves.

Each moved value is replaced by the harvest of the tree that holds all the
changes, compiled with the harvest macros of the two tests; the harvest of
the tree before the series reproduces every old value. The data files carry
one comment block per cause with the old and new value of every moved row and
the TradingView tape of that cause.

Refs #803.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The class series (the ANY basis and same-side exits, the partial close then
reversing entry, the same-id reversal and the re-pin of the frozen digests)
now holds the engine 1.11.1 train, with the reversal exit sizing change and
the other changes merged into it. The only textual conflict was the
unreleased section of the changelog, where every line is kept; the adapter,
its header and the test registrations merged without edits.

Refs #803.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ay which rows are controls

An entry placed after closes on the same bar is a reversal only when those
closes leave part of the position open. The units of the closes are summed
and compared with the held position; the compare now allows the quantity
epsilon that the neighbouring compare of the process-orders-on-close path
already allows, so closes of 0.1 and 0.7 on a position of 0.8, which sum one
unit in the last place below 0.8 in doubles, count as covering it.

The notes of the class tests now say which rows are controls that pass on the
base and carry no tape of their own, and only the row that reproduces a
TradingView tape cites it. The changelog names the two observation fields that
read 0 for an entry placed after a partial close, and the quiet-bar note lists
the counts that move with the re-pin. No pinned value changes.

Refs #803.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The train now also holds the percent default quantity cap (#805). The merge
had no textual conflict: the changelog, the adapter, its header and the test
registrations merged without edits.

Refs #803.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…artial closes do not

The pending order of a reversing entry placed after closes on the same bar
reports created_after_position_close_in_bar as 1 exactly when the units of
those closes reach the held position, and as 0 when they leave part of it
open. The rows read that field on the pending entry before it fills, for
closes of 0.1 and 0.7 on a position of 0.8, closes of 1 and 2 on 3 and two
percentage closes of 50 on 3, and for two controls that leave part of the
position open (closes of 0.1 and 0.6 on 0.8, and one close of 0.25 on 0.8).

The first row needs the quantity epsilon of the cover compare, because 0.1
and 0.7 sum one unit in the last place below 0.8 in doubles. The other two
need the loop to add every close of the bar, not only the first. Each
expectation is derived from one rule, written in the test.

Refs #803.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@luisleo526
luisleo526 merged commit c94dcbd into train/engine-1.11.1 Nov 11, 2026
3 checks passed

@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.

Code graph out of date: Health delta baseline: last indexed commit f5e929d, 44 commit(s) behind this PR's base. Impact and hotspot numbers below may be wrong.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.

Not checked: tests were not run; no formal proof of the changed code.


Graphify review — findings

Fixes a reversing strategy.entry placed after a partial strategy.close so it closes the rest of the opposed position and opens the new one. place_entry now sums the units of every same-bar close and treats the book as flattened only when they cover the whole position; otherwise created_after_position_close_in_bar and prior_close_quantity report 0. A close with unresolved size still counts as covering the whole book. Narrows full-close exit cancellation with an independent_only mode that removes only independent-owner exit rows and leaves relative-exit, shadow and anchored-child rows alone. Flat retirement now keeps a named exit placed later on the same bar as a same-side entry of another id, and cancel_reversal_exit_owners takes the reversed entry id so a FIFO exit ends when a same-id entry reverses its lots. Extends the ANY close rule so a reversing entry's exit percentage is taken from the new entry rather than the whole position being replaced. The new test scenarios encode this behaviour, including a case that expects a requested close to leave the old position open.

Worth a look

  • Early-entry scenario incorrectly expects a requested close to leave the old position open — tests/test_adapter_exit_class_any_same_side.cpp:213 · Escalate · medium
    • unverified hypothesis — survived the adversarial refute pass; needs human review

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

Health — this change adds coupling hotspots:

  • worse: find() — 10 callers, 4 callees
  • new: check_exits() — 1 callers, 7 callees
  • new: check_exits() — 1 callers, 6 callees
  • new: run_case() — 1 callers, 6 callees

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

Health delta baseline: last indexed commit f5e929d, 44 commit(s) behind this PR's base.

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

Test selection

Test selection

1420 of 1420 test file(s) selected (100%) via static blast radius.

Escalated to a full run for safety (noncode-change, uncovered-code) — the selection is not trustworthy on its own (see below). CI should run the whole suite.

  • benchmarks/tests/test_aggregate.py — full-run-safety
  • benchmarks/tests/test_check_provenance.py — full-run-safety
  • benchmarks/tests/test_compare.py — full-run-safety
  • benchmarks/tests/test_public_names.py — full-run-safety
  • benchmarks/tests/test_run_all.py — full-run-safety
  • benchmarks/tests/test_run_strategy_feeds.py — full-run-safety
  • docker/pineforge_sink_finalize_test.py — full-run-safety
  • docker/pineforge_sink_replay_rq_test.py — full-run-safety
  • docker/pineforge_sink_replay_test.py — full-run-safety
  • docker/pineforge_sink_to_json_test.py — full-run-safety
  • docker/quota_columns_test.py — full-run-safety
  • docker/quota_convert_test.py — full-run-safety
  • docker/quota_finalize_test.py — full-run-safety
  • docker/replay_rq_engine_test.py — full-run-safety
  • docker/run_json_artifact_window_test.py — full-run-safety
  • docker/run_json_bin_test.py — full-run-safety
  • docker/run_json_bounded_transport_test.py — full-run-safety
  • docker/run_json_bulk_equity_fuzz_test.py — full-run-safety
  • docker/run_json_bulk_equity_test.py — full-run-safety
  • docker/run_json_codes_test.py — full-run-safety
  • docker/run_json_converter_metadata_test.py — full-run-safety
  • docker/run_json_converter_session_test.py — full-run-safety
  • docker/run_json_diagnostics_test.py — full-run-safety
  • docker/run_json_equity_scan_test.py — full-run-safety
  • docker/run_json_fused_test.py — full-run-safety
  • docker/run_json_harnesses_test.py — full-run-safety
  • docker/run_json_metrics_only_test.py — full-run-safety
  • docker/run_json_misc2_test.py — full-run-safety
  • docker/run_json_misc_test.py — full-run-safety
  • docker/run_json_pfbar_test.py — full-run-safety
  • docker/run_json_provenance_test.py — full-run-safety
  • docker/run_json_quota_supervisor_test.py — full-run-safety
  • docker/run_json_replay_test.py — full-run-safety
  • docker/run_json_selected_sink_test.py — full-run-safety
  • docker/run_json_sink_lexical_test.py — full-run-safety
  • docker/run_json_sink_summary_test.py — full-run-safety
  • docker/run_json_sink_test.py — full-run-safety
  • docker/run_json_sink_vectors_test.py — full-run-safety
  • docker/run_json_spool_work_test.py — full-run-safety
  • docker/run_json_stringio_test.py — full-run-safety
  • docker/run_json_symbol_feeds_test.py — full-run-safety
  • docker/run_json_syminfo_test.py — full-run-safety
  • docker/run_json_trade_scan_test.py — full-run-safety
  • docker/run_json_window_budget_test.py — full-run-safety
  • docker/run_json_wrapper_work_test.py — full-run-safety
  • include/pineforge/native_run_spec.hpp — full-run-safety
  • scripts/derive_corpus_feeds_self_test.py — full-run-safety
  • scripts/fingerprint_self_test.py — full-run-safety
  • scripts/run_corpus_self_test.py — full-run-safety
  • scripts/test_abi_receipt_skips.py — full-run-safety
  • … and 1370 more

non-code file(s) changed (CHANGELOG.md, tests/CMakeLists.txt) → running the full suite for safety (a code graph can't see config/fixture/data deps)

changed code file(s) with no mapped test (CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

· 3 grounded finding(s) anchored inline below; 1 more finding(s) on lines outside this diff (see the check run).

CHECK(got.open_at_end == expected.open_at_end);
}

void check_exits(const Scenario& spec, const std::vector<Bar>& bars, const Observed& got) {

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_exits()

fans out to 7 callees (efferent coupling).

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

CHECK(got.open_at_end == expected.open_at_end);
}

void check_exits(const Scenario& spec, const std::vector<Bar>& bars, const Observed& got) {

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_exits()

fans out to 6 callees (efferent coupling).

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

CHECK(got.exit_from_bracket == (exit_id == "TP" || exit_id == "SL"));
}

void run_case(const Scenario& spec) {

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 — run_case()

fans out to 6 callees (efferent coupling).

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

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