Skip to content

fix(engine): TV fill-time margin admission for default-sized market orders (KI-54/57/58)#80

Merged
luisleo526 merged 1 commit into
mainfrom
fix/tv-margin-admission-gate
Jul 8, 2026
Merged

fix(engine): TV fill-time margin admission for default-sized market orders (KI-54/57/58)#80
luisleo526 merged 1 commit into
mainfrom
fix/tv-margin-admission-gate

Conversation

@luisleo526

Copy link
Copy Markdown
Collaborator

Summary

TradingView silently drops a default-sized (percent_of_equity / cash) market order whose margin requirement exceeds the funds free at the fill. The engine's gate for this was unreachable — guarded by !isnan(qty) while codegen passes qty=na for default sizing — and, when reachable, compared against total equity rather than free funds. Restoring it root-causes three long-standing "unmodellable" parks (KI-30 all-in order-suppression, KI-31 margin cascade, and the KI-57/58 reversal/liquidation divergence) to a single missing admission check plus a sizing-snapshot ordering bug.

Builds on the merged signal-bar sizing freeze (PR #79). At the fill, for an order carrying a signal-bar snapshot:

same_dir    = position open AND order direction matches it
reversal    = position open AND order direction opposes it
held        = same_dir ? |position_qty| * sizing_mark * pv * fx * margin/100 : 0
free_funds  = sizing_equity - held
admit_price = reversal ? slipped(fill_price) : sizing_price
required    = |qty| * admit_price * pointvalue * fx * margin_pct/100
drop iff required > free_funds + eps
  • FLAT open → sized notional; the apply_qty_step floor guarantees it's affordable, so a flat open is never declined however the bar gaps.
  • TRUE REVERSAL → priced at the (slipped) fill; an all-in flip's notional sits within lot-floor slack of equity, so an adverse gap tips it over and TV drops it. Reproduces freddycandle/taro entry tapes exactly (0 engine-only, 0 TV-only).
  • SAME-DIRECTION ADD → free margin is mark-to-market, pinned by a clean-room TV probe (below).
  • KI-58process_margin_call ran after on_bar, so a same-bar liquidation was missing from the frozen sizing snapshot; the engine over-sized and emitted phantom long margin calls. Fixed by re-freezing pending default orders right after a liquidation trims. A strict no-op on bars without one.

The margin basis is TV-pinned, not asserted

Four review rounds argued mark-to-market vs cost-basis free margin with no ground truth. A clean-room probe (data/probes/margin-basis-frac, pct=50, pyramiding=2, where the two conventions give opposite verdicts on the add) settled it: TV admitted 1535/1538 adds while the open lot was underwater → mark-to-market. The all-in control declines all 3169. The engine reproduces both probes 100%.

Scope (each guard has a probe/pin)

Runs only for percent_of_equity, pct ≤ 100, margin ≤ 100, sizing_equity > 0 — the regime where the flat-open invariant holds. CASH, pct > 100, sub-1× leverage, and bankrupt accounts keep the freeze but skip the re-check.

Known cost (disclosed)

Because same_dir/reversal are judged against the engine's position state, an upstream divergence can make a TV-real entry look like an unfundable add: ~106 TV-real entry events are dropped across 5 slugs. Net tier movement is strictly positive.

Test plan

  • ctest81/81; test_margin_admission_gate carries 11 pins (A–K); the slipped-reversal, lot-epsilon, mark-to-market fractional add, bankrupt guard, and margin>100 guard each fail without their respective fix (mutation-verified).
  • run_corpus.sh — 252/252, excellent=239 strong=12 anomaly=1 (unchanged).
  • Downstream validation sweep of 409 community strategies — 22 tier upgrades, 0 downgrades.
  • Independent 3-lens adversarial review (fresh rebuilds): could not refute.

🤖 Generated with Claude Code

TradingView silently drops a default-sized market order whose margin
requirement exceeds the funds free at the fill. The engine's own gate for this
was unreachable for default sizing: it is guarded by !std::isnan(qty), and
codegen passes qty=na whenever the script omits it. When it did run it compared
against total equity rather than free funds.

At the fill, for an order carrying a signal-bar sizing snapshot:

  same_dir    = position open AND order direction matches it
  reversal    = position open AND order direction opposes it
  held        = same_dir ? |position_qty| * sizing_mark * pv * fx * margin/100 : 0
  free_funds  = sizing_equity - held
  admit_price = reversal ? slipped(fill_price) : sizing_price
  required    = |qty| * admit_price * pointvalue * fx * margin_pct/100
  drop iff required > free_funds + eps

Admission price by position state:
  - FLAT open (incl. close-then-reenter): the SIZING notional. For
    percent-of-equity with pct <= 100 the floor in apply_qty_step guarantees
    qty*sizing_price*pv*fx <= sizing_equity, so a flat open is undeclinable no
    matter how the bar gaps. Pricing flat opens at the fill was refuted against
    TV exports.
  - TRUE REVERSAL: the fill price, slipped the way the fill kernel books it.
    An all-in flip's sizing notional sits within lot-floor slack of equity, so
    once the fill gap pushes the requirement past equity TV drops the flip.
  - SAME-direction add: the sizing notional against free margin. An all-in add
    sees free_funds ~= 0 and declines; a fractional add still fills.

held is marked at the same bar close sizing_equity was (sizing_mark), not at
cost basis. Marking it at cost basis leaves free_funds = cash + open_profit
rather than free margin, so the threshold drifts with unrealized PnL in the
wrong direction: an underwater add is dropped though affordable, while a
profitable one is admitted and then immediately margin-called. It also keeps
the gate consistent with process_margin_call, which marks required margin to
the current price.

eps absorbs double rounding AND one whole lot of notional: the quantity was
floored to the lot step, so a decline whose margin is under one lot's worth of
budget is decided by where the floor landed, not by affordability. Nearly half
of all bars on a continuous feed gap by exactly one mintick, so without this
term the reversal branch resolves those bars by lot-floor luck.

Scope: percent_of_equity default sizing with pct <= 100 only — the one regime
where the floor invariant exists. CASH sizing has no equity term (its notional
is unbounded by equity), and pct > 100 breaks the invariant too; both keep the
freeze and skip the re-check.

Also: process_margin_call runs after on_bar, so a same-bar liquidation was not
reflected in the frozen sizing snapshot. The engine then over-sized, the new
position's notional exceeded equity, and the long_full_margin branch emitted a
phantom LONG margin call TV does not have. Rather than moving
process_margin_call (which would change what strategy.equity reads inside
on_bar for every strategy), re-freeze any still-pending default-sized market
order placed on this bar right after a liquidation actually trimmed something.
A strict no-op on bars with no margin call.

Gates: ctest 81/81. test_margin_admission_gate pins flat gap-up admitted,
all-in same-dir add declined, fractional add admitted, reversal on adverse gap
declined, reversal at exact tie admitted, CASH flat open admitted, slipped
short reversal on a zero-gap bar admitted, reversal within one lot of notional

REVISION 4 — scope tightened after three adversarial gate reviews:
  * The gate runs ONLY where its flat-open invariant actually holds:
    percent_of_equity, pct <= 100, margin <= 100, and sizing_equity > 0.
    apply_qty_step returns qty UNFLOORED for qty <= 0, so on a bankrupt
    account |qty|*sizing_price == |sizing_equity| while free_funds < 0 and the
    gate would decline every order forever, flat opens included (one harness
    candidate went 3659 entry rows -> zero trades). margin > 100 breaks the
    invariant the other way: required = equity*pct/100*margin/100 > equity.
  * A FRACTIONAL same-direction add is no longer gated. Whether TV measures
    free margin against cost basis or mark-to-market flips the verdict there,
    and no export discriminates the two; TV also demonstrably admits some
    over-allocated entries and trims them in-bar with a margin call, so a
    silent decline would not obviously be more faithful. Only the all-in add
    is gated -- unfundable under every convention, and the one case a real
    export pins.

ctest 81/81; test_margin_admission_gate now carries 11 pins, of which the
slipped-short-reversal, the one-lot epsilon and the bankrupt-account guard
each FAIL without their respective fix. Corpus 252/252 unchanged. Sweep of 409
community strategies: 22 up, 0 down.

REVISION 5 — the same-direction add's free-margin basis is now PINNED by a
clean-room TradingView probe (data/probes/margin-basis-frac, pct=50,
pyramiding=2). At pct=50 mark-to-market and cost-basis give OPPOSITE verdicts on
the add; TV admitted 1535/1538 adds while the open lot was underwater and
declined the in-profit ones -> mark-to-market. The engine reproduces that probe
100% (4706/4706 TV trades; the all-in control declines 3169/3169 adds). So the
fractional add is no longer exempted: it is gated against mark-to-market free
margin like the all-in add, which is what TV does. Pin I now asserts the
in-profit fractional add is DECLINED (fails under a cost-basis rule).
@luisleo526 luisleo526 merged commit 94bbfef into main Jul 8, 2026
5 checks passed
@luisleo526 luisleo526 deleted the fix/tv-margin-admission-gate branch July 8, 2026 21:59
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