Our broker emulator ran the margin gate at fill time using next-bar open; TradingView runs it at signal time using current-bar close. A 3-cent gap between those prices was enough to reject an order sized at full equity. How six isolation probes traced a community strategy’s PnL drift to that margin check.
The figures in this post were measured on May 9, 2026 and are kept as they were. For current results, see how we measure parity.
A community Pine strategy in our parity corpus
called IES was the only strategy at moderate parity against TradingView
once we graded every strategy on strict thresholds. Counts agreed. Entry
prices agreed to the cent. Exit prices agreed to the cent. But by April 2026
cumulative PnL had drifted apart by about $29,000.
This is the story of how we found the cause: a single margin check that ran one bar too late.
The asymmetric fingerprint
Here's what verify_corpus.py was reporting on the strategy before we
started digging:
community/IES (pre-fix)
Profile: strict
TV trades: 2668 (raw 2668)
Engine trades: 2663 (raw 2663)
Matched: 2632 (98.7% of TV)
Count delta: 0.1874% (OK)
Entry-price p90 delta: 0.0000% (OK)
Exit-price p90 delta: 0.0000% (OK)
PnL p90 delta: 2.5412% (X)
-> moderate
That row is diagnostic by itself. When count, entry price, and exit price all agree but per-trade PnL drifts by 2.5%, you are not looking at a signal-logic bug or a fill-price bug. Both engines pick the same trades at the same prices. They size them differently.
Near the end of the comparison window, the engine's qty on every matched
trade was about 97.3% of TradingView's, the same ratio as the two accounts'
equity. The divergence lived in strategy.equity (the
recursive feedback variable that every position-sizing formula depends
on), not in any per-trade arithmetic.
What we tried first (and why none of it worked)
Before the right hypothesis we burned through three wrong ones. They are worth recounting because they are the realistic shape of debugging: most of the work is killing hypotheses, not confirming them.
Indicator warmup divergence. The engine warms its TA stack with five
months of OHLCV before the comparison window opens; TradingView's chart
history may not extend that far. We built a per-bar TA tracing harness
and found nine real indicator bugs in the process: ta.pivothigh /
ta.pivotlow semantics, ta.iii formula, ta.tsi codegen and scaling,
ta.cog weighting, ta.bbw percentage, ta.vwap daily anchor, the
ta.supertrend line value, ta.pivot_point_levels previous-bar HLC, and
ta.cross skip-tie semantics. We fixed all nine. None of them lifted IES
out of moderate.
Stop / limit fill timing. A different community strategy
(scalping-wunder-bots) had a clean pattern of the engine exiting one
bar earlier than TradingView. We hypothesized that the engine evaluated
strategy.exit stops on the entry bar while TV deferred to the next
bar. We built parity-probe-01-stop-limit-timing to isolate exactly
that. It returned 778 of 778 trades matched, all four strict gates
clean. The hypothesis was falsified.
CHoCH / BOS swing tracking. Another strategy (VCP) had one
spurious entry on 2025-05-12 12:15 that we suspected came from
divergent pivot-state evolution. We built
parity-probe-02-choch-bos-isolator mirroring its pivot signature
exactly. 1026 of 1026 matched. Falsified again.
Three hypotheses in, we had no IES improvement and one piece of new methodology: writing minimal Pine strategies that isolate one hypothesis at a time was massively faster than tracing internal variables inside a 700-line community script. So we wrote a third probe.
The probe that found it
The trick with parity-probe-03-equity-mirror is that the order
quantity itself encodes strategy.equity:
//@version=6
strategy("Parity probe 03 - equity mirror",
shorttitle="par_p03", overlay=true, initial_capital=1000000,
default_qty_type=strategy.fixed, default_qty_value=1,
pyramiding=0, process_orders_on_close=false,
commission_value=0, slippage=0)
bool fire = dayofweek == 2 and hour == 0 and minute == 0
and strategy.position_size == 0
float qty_dyn = math.round(strategy.equity / close * 1000) / 1000
if fire and qty_dyn > 0
strategy.entry("E", strategy.long, qty=qty_dyn)
if strategy.position_size > 0 and bar_index >
strategy.opentrades.entry_bar_index(0)
strategy.close("E")
qty = round(strategy.equity / close * 1000) / 1000 rounded to three
decimals means the exported Size (qty) column in TradingView's "List
of Trades" CSV becomes a per-bar oracle of TV's strategy.equity value.
Match engine and TV trades by entry timestamp, line up the qty columns,
and you read the equity divergence directly off the diff.
The first eight matched trades:
2025-03-31 00:15 eng_qty=553.1490 tv_qty=553.1490 diff=+0.000%
2025-04-14 00:15 eng_qty=612.9970 tv_qty=612.1600 diff=+0.137%
2025-04-21 00:15 eng_qty=623.1630 tv_qty=622.3110 diff=+0.137%
2025-08-25 00:15 eng_qty=207.2020 tv_qty=209.8110 diff=-1.244%
2025-11-03 00:15 eng_qty=242.4760 tv_qty=251.9860 diff=-3.774%
2025-11-24 00:15 eng_qty=339.1820 tv_qty=361.0170 diff=-6.048%
2025-12-22 00:15 eng_qty=307.0180 tv_qty=330.5680 diff=-7.124%
2026-01-05 00:15 eng_qty=295.2650 tv_qty=320.1020 diff=-7.759%
Trade 1 matched perfectly. Both sides started with $1M of equity. From trade 2 onward the qty columns separated and never reconverged. The gap stabilised around 7.76% by trade 8.
The cause was not a calculation drift. It was a missed trade. The entry that would have been trade 2 (2025-04-07) appeared in TV's trade list but not in the engine's. TV took the trade and absorbed a $1,345 loss, its equity falling to roughly $984k. The engine never took the trade, so its equity stayed at roughly $985k. The engine was sized 0.137% larger than TV on the next two trades; after that the two sides accepted and rejected different Mondays, the cumulative losses diverged on different equity paths, and the gap never closed.
A ghost fill
We added std::cerr logging to strategy_entry, process_pending_orders,
enter_market_from_flat, and apply_market_order_fill, all gated by an
environment variable so the noise didn't leak into other test runs.
At the signal bar, 2025-04-07 00:00:
[entry CALL] ts=1743984000000 bar=16236 id=E is_long=1 qty=627.312
pending_orders.size=0 pos_qty=0 pos_side=0
strategy.entry was called. The order was queued. So far, fine.
At the fill bar, 2025-04-07 00:15:
[ppo] ts=1743984900000 bar=16237 pass=0 i=0 id=E type=0 is_long=1
qty=627.312 eligib=FILL_TRY
-> fill.kind=0 fill_price=1566.69
-> AFTER apply: pos_qty=0 pos_side=0 pyramid.size=0 trades.size=14
fill.kind=0 is FillEvaluation::Kind::Fill. The price evaluator said
fill it. apply_filled_order_to_state was called. And the position
state did not update. A ghost fill.
One more debug line, this time inside enter_market_from_flat, printing
the inputs to the margin guard:
[margin] ts=1743984900000 qty=627.312 fill_px=1566.69 margin_pct=100
required=982803 available_equity=982785 net_profit_sum=-17215.4
initial=1e+06 block=YES
Required margin: $982,803. Available equity: $982,785. The margin guard wanted about $19 more than the account had, returned early without updating any state, and produced no error and no public log line. From the strategy's perspective, the trade simply did not exist. (This debug run traded from the start of the warmup data, so its equity and qty differ slightly from the comparison run below.)
Why TradingView took the same trade
Here is the OHLC for the relevant bar pair:
04-07 00:00 (signal): O=1579.98 H=1585.57 L=1566.00 C=1566.66
04-07 00:15 (fill): O=1566.69
The probe sized the order at signal close: qty = round(985319.43 / 1566.66 * 1000) / 1000 = 628.93. Required margin computed at signal
close exceeds available equity by only four cents. Required margin computed
at the next bar's open exceeds it by $18.91.
| Price used | Required | Equity | Overshoot |
|---|---|---|---|
| Signal close ($1566.66) | $985,319.47 | $985,319.43 | +$0.04 |
| Fill open ($1566.69) | $985,338.34 | $985,319.43 | +$18.91 |
A three-cent move in ETH, multiplied by 628.93 contracts, added $18.87 of required margin, for a total overshoot of $18.91. The engine's margin check happened at fill time using the fill price and rejected the trade. TradingView's broker emulator, we now believed, ran the same check at signal time using the signal close and accepted the trade, because four cents on a $985k position is below whatever floating-point tolerance TV applies at the boundary.
Triangulating with three more probes
To prove this wasn't a one-trade coincidence and that the bug was specifically about the 1× equity boundary, we built three more probes that bracket it:
| Probe | Sizing | Margin pressure |
|---|---|---|
parity-probe-04-percent-of-equity-sizing | default_qty_type=percent_of_equity, default_qty_value=99 | ~99% equity |
parity-probe-05-small-equity-fraction | qty = round(equity / close * 100) / 1000 | ~10% equity |
parity-probe-06-edge-margin-sizing | qty = round(equity * 0.5 / close * 1000) / 1000 | ~50% equity |
For all three probes, we compared the engine's trade list with the TradingView trade list (Strategy Tester › List of trades):
parity-probe-04: TV=57 engine=57 matched=57/57 (100.0%) PnL p90=0.0006% → excellent
parity-probe-05: TV=57 engine=57 matched=57/57 (100.0%) PnL p90=0.0698% → excellent
parity-probe-06: TV=57 engine=57 matched=57/57 (100.0%) PnL p90=0.0700% → excellent
When sizing wasn't pinned to the 1× margin boundary, the engine and TV agreed perfectly across all 57 trades on each probe. The bug only manifested at the boundary, where slippage between signal close and fill open could flip the margin check.
The fix
Move the margin check from enter_market_from_flat (which runs at fill
time, using the fill price) to strategy_entry (which runs at signal
time, using the current bar's close):
void BacktestEngine::strategy_entry(const std::string& id, bool is_long,
double limit_price, double stop_price,
double qty, ...) {
if (!trading_is_active(...)) return;
+
+ // TradingView broker rule: market-entry orders are admitted only
+ // when qty * <signal-bar close> * margin_pct/100 <= equity. Check
+ // happens HERE at signal time with current_bar_.close, NOT later
+ // in apply_market_order_fill where fill_price is the next bar's
+ // open. Verified empirically by parity-probe-{03..06}.
+ if (!std::isnan(qty) && std::isnan(limit_price) && std::isnan(stop_price)) {
+ double margin_pct = is_long ? margin_long_ : margin_short_;
+ if (margin_pct > 0.0 && !std::isnan(current_bar_.close)) {
+ double required = std::abs(qty) * current_bar_.close * (margin_pct / 100.0);
+ double available = current_equity();
+ double epsilon = std::max(1e-9, std::abs(available) * 1e-12);
+ if (required > available + epsilon) {
+ return;
+ }
+ }
+ }
...
About a dozen lines in strategy_entry (the commit carries a longer
comment), plus the matching eight-line deletion from
enter_market_from_flat. Re-run on IES:
community/IES (post-fix)
TV trades: 2668 (raw 2668)
Engine trades: 2643 (raw 2643)
Matched: 2618 (98.1% of TV)
Count delta: 0.9370% (OK)
Entry-price p90 delta: 0.0000% (OK)
Exit-price p90 delta: 0.0000% (OK)
PnL p90 delta: 0.7809% (OK) ← was 2.5412%
-> excellent
PnL drift went from 2.54% to 0.78%. IES moved from moderate to
excellent. Across the rest of the corpus at the time: 165 of 168
strategies at excellent, 2 at strong, 0 at moderate, and probe-03
at weak (below). All 16 ctest binaries passed, all 313 codegen pytest
cases passed, and there were no regressions on any of the other 161
strategies.
The plot twist
After the fix, parity-probe-03-equity-mirror itself still didn't
fully match TV. The engine took 25 entries, TV took 24, and only 13 of
those overlapped. Probe-03 is designed to live exactly on the 1× equity
boundary, so any tolerance disagreement between the two implementations
shows up there before anywhere else.
For each of the 57 Mondays in the comparison window we computed qty * signal_close - equity and recorded TV's accept-or-reject decision. Eight
of them:
| Monday | Overshoot at signal close | TV action |
|---|---|---|
| 2025-04-07 | +$0.04 | TAKE |
| 2025-04-14 | -$0.76 | TAKE |
| 2025-04-21 | -$0.19 | TAKE |
| 2025-04-28 | +$0.34 | REJECT |
| 2025-05-12 | -$0.18 | REJECT (under equity, yet rejected) |
| 2025-05-26 | -$0.89 | REJECT (under equity, yet rejected) |
| 2025-08-18 | +$0.24 | REJECT |
| 2025-11-10 | +$0.24 | TAKE (identical overshoot, opposite outcome) |
Aug 18 and Nov 10 carry identical +$0.24 overshoots and produce opposite outcomes. No pure margin-check rule (any threshold, any tolerance) can produce that pattern. TV's broker emulator at this boundary depends on something that isn't visible in any documented Pine variable: chart-load history, the order in which bars became visible to the strategy when the user published, or some hidden internal state we have no way to observe.
The honest read is that this is an undocumented corner in TradingView's
broker emulator that only manifests when sizing is pinned exactly to
the 1× equity boundary. Real strategies rarely sit exactly on the
boundary. community/IES comes closest: it sizes each trade from a small
risk budget and its stop distance, so some of its orders use nearly all of
the account's equity, and it still reached excellent after the fix. We
don't expect the corner to matter much in real use. We chose to keep the fix and document probe-03 as a known
limitation. The cosmetic tier downgrade on the diagnostic probe is a
fair price for the IES win.
Six things this debugging story taught us
1. Asymmetric metric fingerprints diagnose the bug class. When count, entry price, and exit price all agree but PnL drifts, you are looking at a sizing bug. We should have looked at sizing-related code before auditing indicator formulas.
2. Isolation probes scale better than tracing big strategies. Six small Pine probes, each testing one hypothesis, found the bug. Tracing every internal variable in a 700-line community script with equity-feedback computation would have taken longer and produced more noise. This is the same reason we write unit tests instead of debugging only at the integration level.
3. TradingView's qty= parameter has signal-time margin semantics.
The check uses the bar at which strategy.entry is called, not the
fill bar. This isn't documented in Pine's reference. Anyone
implementing a Pine-compatible broker emulator should match it; the
natural implementation puts the check at fill time (where fill price
is known) and silently breaks.
4. Floating-point at boundaries is always the enemy. The single
trade that split probe-03's two equity paths was a four-cent overshoot
on a $985k position. Production strategies should leave a 1 to 5%
margin buffer (a * 0.99 or * 0.95 in the sizing formula) to avoid
living on the boundary at all.
5. Compounding magnifies single-trade misses. In probe-03 the 04-07 miss alone was $1,345; by trade 8 the two sides' sizes were 7.76% apart and stayed there, because each missed trade shifted the equity path, which changed the next sizing, which changed the next accept-or-reject decision. In IES, the same rule over 13 months of trades added up to a gap of about $29,000. Equity-feedback strategies are unforgiving about isolated execution gaps.
6. Sometimes the bug is on the other side, and you document it honestly. TV's behavior at the 1× boundary is a real corner we can't fix from our side. Documenting it candidly beats silently chasing a metric that can't be matched.
The methodology generalises beyond this specific bug. Parity probes are the complement to our PyneCore cross-validation sweep: PyneCore tells us when our engine differs from another engine; parity probes tell us why it differs from TradingView. When we find a new gap, the playbook is now: write the smallest Pine script that exhibits it, diff the trade lists, instrument the engine, fix the disagreement, document any TV corner that can't be reconciled.
Where to go from here
- Transpile and backtest from Claude or Cursor: the local MCP
server's
transpile_pinetool returns the C++ that PineForge generates from your Pine source; the margin check itself runs inside the engine runtime, not in that generated code. - Browse the gallery: today's 312 open-corpus strategies, each compared trade by trade with TradingView.
- How we measure parity: the current results and their sources.