fix(engine): widen pending-aware gross admission to live-position pairs#138
Merged
Conversation
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>
There was a problem hiding this comment.
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-barstrategy.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 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 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() |
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.
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'sown frozen notional at the booked fill price. On a reversal
held = 0, so it chargesnothing 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
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.
engine_trades.csvbyte-identical to base (diff -rqclean). Zero mechanical DOWN.baseline at the parent commit. Only 4 of 499 verify artifacts changed at all.
pnlP90_ok,countDelta_ok,entryP90_ok,exitP90_okpass→fail counts are all 0;countAbsDelta 0→nonzerois 0;coverage drops >0.05pp: 0. A pre-registered at-risk population of 41
pnlP90-passing and 31 excellent rows is byte-identical.
holds excellent / countAbsDelta 0 / 100.0% on both arms.
Fresh-context adversarial R7 gate: PASS.
Residuals a reviewer should accept explicitly
(
test_margin_admission_gate, case M.5, 2 asserts). M.5 asserted that alive-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.
pnlP90worsens while their count, coverage and match allimprove — the all-in equity cascade re-shuffling per-trade P&L. Neither was passing
pnlP90before, 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.
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