Skip to content

fix(engine): widen pending-aware gross admission to live-position pairs#138

Merged
luisleo526 merged 1 commit into
mainfrom
fix/pending-aware-widen-20260725
Jul 25, 2026
Merged

fix(engine): widen pending-aware gross admission to live-position pairs#138
luisleo526 merged 1 commit into
mainfrom
fix/pending-aware-widen-20260725

Conversation

@luisleo526

Copy link
Copy Markdown
Collaborator

The bug

TradingView admits an order at placement time using the gross movement the
order would cause, counting any opposite sibling already pending on the same source
bar. The engine had no equivalent placement-time gate for two cases, so it filled
both legs of an opposite same-bar pair that TradingView admits only one of.

The existing fill-time gate does not cover this. That gate is per-order and net:
its budget is sizing_equity − held(same-direction only) and its cost is the order's
own frozen notional at the booked fill price. On a reversal held = 0, so it charges
nothing for the position being closed, and it carries no term for a sibling queued
on the same source bar
— by the time the second order fills, the first has already
become the live position and its margin reads as released. So an unaffordable pair
passes it twice.

The fix

Extend the existing gross admission path to two cases previously excluded as
controls: live-position reversals and default sizing. The gross rule erases a
rejected order, so the fill-time gate never sees it and the two cannot double-count.

One term is load-bearing and easy to get wrong: an earlier call that is already over
the pyramiding cap at placement moves nothing, and must contribute zero
. Without
it the rule over-rejects. Concordance on real validation data: live side FLAT → later
declined 84/84; live side SHORT → later declined 159/159; live side LONG with the
earlier call over cap → later admitted 12/12. 255/255, no counterexamples.

Evidence

  • ctest 122/122. New 8-case suite is a genuine RED against base — base sources
    plus these test files only, fresh build dir: 17 passed / 12 failed; patched
    29 / 0. Reproduced independently on a second platform (Linux/x86-64/g++ 11.4 as
    well as macOS), with base and RED libraries byte-identical in size to rule out a
    stale-library artifact.
  • Corpus gate: 252 ran, ok=252, 247 excellent / 4 strong / 1 anomaly, and all 252
    engine_trades.csv byte-identical
    to base (diff -rq clean). Zero mechanical DOWN.
  • Validation board: 403/408 → 404/408, 1 UP / 0 DOWN against a freshly built clean
    baseline at the parent commit. Only 4 of 499 verify artifacts changed at all.
  • No gate-flag flip anywhere: pnlP90_ok, countDelta_ok, entryP90_ok,
    exitP90_ok pass→fail counts are all 0; countAbsDelta 0→nonzero is 0;
    coverage drops >0.05pp: 0. A pre-registered at-risk population of 41
    pnlP90-passing and 31 excellent rows is byte-identical.
  • 9/9 admission probes byte-identical; the dedicated pending-aware oracle probe
    holds excellent / countAbsDelta 0 / 100.0% on both arms.

Fresh-context adversarial R7 gate: PASS.

Residuals a reviewer should accept explicitly

  1. A pre-existing test pin was corrected in the commit that makes it pass
    (test_margin_admission_gate, case M.5, 2 asserts). M.5 asserted that a
    live-short all-in pair fills both legs; that encoded prior engine behaviour rather
    than a TradingView pin, and it is absent from that suite's own enumerated pin list.
    Its warrant is the 159/159 live-short concordance above — tape-derived, not a
    dedicated export. Independently bounded: compiling the unmodified base test file
    against the patched library gives 72 passed / 2 failed, both inside M.5, with
    nothing else in the 74-assert suite moving. Precedent: Extend coof cascade eligibility to strategy.exit orders (KI-67) #98.
  2. Two validation rows' pnlP90 worsens while their count, coverage and match all
    improve — the all-in equity cascade re-shuffling per-trade P&L. Neither was passing
    pnlP90 before, so neither loses a tier, and no passing row was pushed to failing.
    For one of the two the reported P&L delta is partly a ladder candidate-selection
    artifact
    and should not be quoted as a clean measure of this change's P&L effect;
    its count and coverage gains are robust.
  3. Environment scope. The gate ran with a compiler pin and at a codegen revision
    that is constant across both arms — the A/B is valid, but the absolute tier split
    should not be diffed row-by-row against artifacts built elsewhere. Excellent+Strong
    totals agree.

🤖 Generated with Claude Code

TradingView costs the LATER of two opposite same-source-bar strategy.entry
calls as a GROSS movement -- its own requested position plus the movement the
earlier pending opposite call will make -- and silently declines it when that
exceeds the margin budget, even though the desired residual position would be
affordable. The shipped rule
(finalize_default_flat_market_gross_admission, PR #113) only ran when the pair
was queued from TRUE FLAT with an exact two-object book. Every real specimen of
this idiom queues the pair while a LIVE position is held, and queues
strategy.close legs alongside the entries, so the rule never fired on the board.

Widened past exactly two of the controls the KI-65 pending-MARKET oracle carved
out, and nothing else:

  * the pair may be queued while a live position is held
    (default_flat_market_gross_scope_is_live no longer requires FLAT; the
    eligible() predicate no longer requires created_position_side == FLAT or
    !created_after_position_close_in_bar; both calls must still agree on
    created_position_side AND created_position_cycle_seq)
  * the book may additionally contain that source bar's own unpriced
    __close__* EXIT legs (they transact no new margin and the qty/equity/mark
    triple both candidates carry was frozen before they existed). Any other
    member -- priced, raw, bracket, OCA, or ANY order carried in from an
    earlier bar -- abandons the adjudication. The created_seq/incarnation
    consecutiveness check is relaxed by exactly the number of intervening
    close legs, so a three-call book still cannot masquerade as a pair.

One term the naive reading of the rule does not have, and needs: an earlier
call that was already over the pyramiding cap at placement moves nothing, so it
contributes ZERO to the later call's budget. Without it the rule would convert
12 currently-correct real-row admissions into declines.

  live side at placement | earlier "Long"    | later "Short"  | TradingView
  -----------------------|-------------------|----------------|------------
  FLAT                   | opens   (counts)  | 100+100 > 100  | declined
  SHORT                  | reverses (counts) | 100+100 > 100  | declined
  LONG                   | over cap (zero)   | 100 + 0 <= 100 | ADMITTED

Relationship to the fill-time margin admission gate (48363a1, unchanged): that
gate is a per-order NET test -- budget = sizing_equity minus the margin a
SAME-DIRECTION open position ties up, cost = the order's own frozen notional at
the booked fill price. On a reversal it charges nothing for the position being
closed, so it has no term for a sibling queued on the same bar and cannot reach
this class. The two are complementary and cannot double-count: this rule runs at
the broker boundary and ERASES the rejected order, so the fill-time gate never
sees it; anything this rule admits reaches it with an unmodified quantity.

Evidence. TradingView oracle: data/probes/pf-probe-ki65-pending-market-
affordability (3,128 native trades, 6 discriminating cells) stays at 100% /
cAbs 0. Real-row confirmation at zero export cost, over every board row whose
engine tape prints both sides on one bar and both tapes agree on the side held
entering it: 84/84 flat, 159/159 live-short declined, 12/12 live-long admitted
-- 255/255, zero counterexamples, across fluxchart-supply-and-demand-zones,
market-logic-india-low-lag-strength-oscillator, chartprime-power-order-blocks
and cntvxiao-smc-vsa-oi. Negative control for the ARITHMETIC rather than a
shape match: twelve further rows do print both sides and TradingView prints both
too -- every one of them sizes at percent_of_equity <= 10 or default FIXED,
where own + earlier <= equity. Those rows stay out of scope via the unchanged
default_qty_value == 100 guard; gb2wgkrtxs-liquidity-sweep-strategy-m2-swing
(992 such bars, excellent, cAbs 0) is the sensitive sentinel for it.

Still excluded, deliberately: priced entries, explicit qty (own signal-time and
fill-time gates; the POOC+COOF explicit pair has its own oracle), raw
strategy.order, same-direction pairs, cross-bar pairs, OCA siblings,
pyramiding != 0, POOC/COOF, magnifier, non-zero commission or slippage,
percent_of_equity != 100, margin != 100, any active risk policy. The mirrored
call ordering (the Short call first) is a principled consequence of the same
inequality but is NOT measured -- no board row writes it that way.

Gates: ctest 122/122. New suite test_live_position_market_gross_admission (8
cases, 29 asserts) is a genuine RED against bddddac -- 17 passed / 12 failed,
including both live-short position assertions. One corrected pin:
test_margin_admission_gate M.5 asserted that the all-in live-short opposite pair
fills BOTH legs, which was the pre-widening engine behavior rather than an
independent TradingView pin; its stated purpose (proving the GB2 fixed/explicit
gate does not reach percent_of_equity) is unchanged and still proven. All nine
admission-family probes byte-identical against a clean bddddac worktree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 25, 2026 14:46
@luisleo526
luisleo526 merged commit c31996c into main Jul 25, 2026
8 checks passed
@luisleo526
luisleo526 deleted the fix/pending-aware-widen-20260725 branch July 25, 2026 14:50

Copilot AI 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.

Pull request overview

This PR broadens the KI-65 “pending-aware gross movement” admission gate so it also applies when two opposite default-sized MARKET entries are queued on the same source bar while a live position exists (and when default sizing is used), matching TradingView’s placement-time gross affordability behavior and preventing both legs of an unaffordable same-bar opposite pair from filling.

Changes:

  • Widened finalize_default_flat_market_gross_admission() scope and book-shape handling to include live-position pairs and co-queued unpriced same-bar strategy.close() legs.
  • Refactored entry placement snapshot logic so the gross-admission candidate can share the “all-in default sizing” snapshot without inheriting the true-flat requirement.
  • Added a dedicated 8-case unit test suite for live-position pending-aware gross admission and updated an existing margin admission test expectation.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/test_margin_admission_gate.cpp Updates M.5 expectation/commentary to reflect the widened gross-admission behavior.
tests/test_live_position_market_gross_admission.cpp New regression suite covering live-position opposite MARKET/MARKET pairs and control shapes.
tests/CMakeLists.txt Registers the new test executable in the test build.
src/engine_strategy_commands.cpp Extracts a reusable “default all-in sizing snapshot” and broadens gross-admission candidacy.
src/engine_fills.cpp Widened pending-aware gross admission: live-position scope + allow co-queued unpriced close legs + over-cap movement term.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/engine_fills.cpp
Comment on lines 1364 to 1366
const double notional_k = syminfo_.pointvalue
* active_account_currency_fx();
if (!std::isfinite(notional_k) || !(notional_k > 0.0)) return;
Comment thread src/engine_fills.cpp
Comment on lines +1266 to +1270
const bool same_bar_market_close =
other.type == OrderType::EXIT
&& other.created_bar == source_bar
&& other.id.rfind("__close__", 0) == 0
&& other.oca_name.empty()
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.

2 participants