v6.8.10
This release is almost entirely about the strategy simulator: how TradingView sizes, books and settles a position down to the last ULP, and when it lets a risk rule halt trading. Alongside it, four smaller divergences found while running the wild corpus — the intraday session mask, two request.security transformer crashes, math.pow and ta.cross — and two broker sync-engine fixes from live sessions.
Strategy engine — TradingView exactness
strategy.risk.max_drawdownnow matches TradingView's semantics. The rule is enforced against the realized (closed-equity) high-water mark with a mark-to-market drop, and only on bars that book realized P&L. Open-position floating drawdown alone no longer halts trading: a position rides through paper losses and force-closes the instant a reducing order fills while the drawdown is past the limit. Measured with TradingView probes — a single long sitting 50% underwater with an 8% limit never halts until a close order fires.- Exit orders settle per entry, not per entry id. TradingView keeps two views of an open position: the trade rows (
strategy.opentrades,opentrades.size,position_avg_price) settle FIFO across the whole position, while every exit or close order settles against the entry it was bound to, and an entry holds its pyramiding slot until its own bound quantity is spent. The per-id ledger is replaced by a per-fill binding book: onestrategy.exitleg per filled entry, each with its own OCA reduce group and its own entry fill price for tick offsets, and pyramiding counted on the binding book. - Quantities live on TradingView's 1e-8 unit grid. Every quantity is carried as an integer count of 1e-8 units and handed out as
units * 1e-8, which differs from the plain lot value on 13-15% of quantities (104 lots of 1e-5 is0.0010400000000000001, not the0.00104that104 / 1e5yields). A no-op on whole-contract symbols. Measured on BINANCE:BTCUSDT, CAPITALCOM:EURUSD, CAPITALCOM:US500, OANDA:XAUUSD, FX:EURUSD and NASDAQ:AAPL. position_avg_priceis derived, not accumulated. It is the cost summed over the open trades oldest-first divided by the running position size — not by the sum of the leg sizes; the two differ by ULPs once a leg is partially closed. The carried accumulator is gone and the average is re-derived on every fill.- Position size is a long lot ledger. TradingView books every position change as a long count of lots and materializes
lots * mincontract. Subtracting the materialized doubles instead drifts by one ULP on ~40% of partial-close remainders, and summing the per-trade doubles getsposition_sizewrong on ~45% of bars. Sizes are now added in the lot domain everywhere, including the FIFO remainder, the grid reaches up to TradingView's signed-long range, and the snap width is a few ULPs of the scaled value instead of a relative epsilon that grew to a third of a lot at 3e11-lot counts. All of it inert below 2^53 units. Isolated on BINANCE:SHIBUSDT 1h, where a 1e-3 lot step on ~1e-5 prices makes every arithmetic detail observable. - Default sizing quantizes its money budget. Below 1e7 account currency, TradingView rounds the money side of default sizing to 10 significant decimal digits (half-up) before dividing by the unit cost, then truncates the quotient with no near-integer snap. This replaces the previously measured sub-1e7 snap-up bands, whose 0.05 and 0.005 tick edges on BINANCE:BTCUSDT turn out to be exactly half of that decimal grid expressed in ticks.
strategy.netprofitis derived from the gross figures. TradingView keeps no running accumulator: a+= profitchain drifts by ULPs from the sixth close onwards, whilegrossprofit - grossloss - open_commissionis bit-exact over 1119 closed trades.netprofitis now a read-only property over those.- A gapped exit leg survives the reversal that shares its bar open. When a
strategy.exitleg and the entry reversing the position gap through the same bar open, the leg is no longer cancelled by the reversal: it sells its own quantity again and opens a position under its own exit id, and a position opened that way takes its exit leg first, so afrom_entry-lessstrategy.exitsplits the older trade as it closes everything. The gap batch fills entries before exits and arms only after the market orders queued on an earlier bar have filled, so a plain market reversal still cancels the leg outright. strategy.cancel_all()now cancels pending market orders too. It cleared the entry and exit books but not the market queue, so a market entry cancelled on its placement bar still filled at the next open.- Stop and limit levels snap onto the tick grid with a 1e-4 tick tolerance, instead of a fixed 7-decimal rounding of the tick count.
Session, security, math and ta
- The intraday session mask admits a bar by its opening time. It used to admit by overlap; TradingView admits by
open <= bar_open < closealone. A session that fits inside one bar therefore matches no bar at all, and a bar longer than the session is in it only when the bar opens inside it. Measured on BINANCE:BTCUSDT at 30 minutes, 4 hours and daily. This is what madestrategy.close_all(when = endSession)with a"1550-1555"session fire on every 15:30 bar of a 15-minute chart. ticker.heikinashi(<local>)no longer raisesNameErrorat import. A call was judged module-level by its callee alone, so alib.*call with a local argument was emitted into the module-level security contexts. Every argument and keyword now has to be module-level as well.- A tuple
request.security()outside an assignment no longer crashes the security child. When the call is not the direct right-hand side of an assignment — a nested function'sreturn, say — the arity could not be read off the unpack target, so it fell back to a scalarnaand the unpack failed. The tuple expression now pins the arity itself. math.powtakes TradingView's exponent shortcuts. For exponents 2, 1, 0 and 0.5 TradingView answers with the shortcut result exactly; the platformpow()behind**disagrees on some bases, which a recursive script carries into its output. Measured: only these four —pow(x, -1)is not1 / xon TradingView, andpow(x, 3)is notx * x * xon most bars.ta.crossis notta.crossoverorta.crossunder. It keeps its own three-state armed flag and starts unarmed, so an equality plateau present from the start (a warmup saturation, say) never fires either way, while a strict relation before an equality run still arms the jump after it.ta.crossover/ta.crossunderare pure classic and exact — the immediate previous bar's raw at-or-below/at-or-above relation plus a strict jump, with no armed state and no tolerance — which reverts the mistaken armed rule that over-suppressed an equality plateau entered from above.
Broker plugins
- An expired cancel grace probes the venue before halting. A cancel-tentative entry re-armed from the journal after a restart carries its original mark-time anchor, so the stale grace is already expired on the first drive of the new run and the halt fired with zero retries — skipping the authoritative cancel probe entirely, so every restart re-latched the degraded halt even when the venue could resolve the disposition immediately. The promotion now also requires at least one cancel attempt in the current session.
- A re-issued entry with identical parameters dispatches again. A fully filled entry stays in the active-intent book as the diff sentinel, so a fresh
strategy.entrywith the same id and the same parameters compared equal and never dispatched — while the strategy runtime sized thefrom_entryexit to fill plus pending, arming a bracket over quantity that could never arrive (on spot this oversold into the wallet baseline). Dispatches are now pinned to the Pine order instance they were built from, and a value-equal re-emission backed by a new instance over a consumed fill dispatches a fresh entry cycle, mirroring the simulator's fill-time pyramiding-cap drop. - A filled
close_allretires its slot. Its fill carries no entry id, so neither the per-entry flat teardown nor the keyed-close retire ever popped the slot: the run's first filledclose_allstayed active forever and the irreversible-market-close guard silently swallowed every later one. Id-less close fills are now attributed to theclose_allslot when the venue order id belongs to its dispatch, and the slot retires on completion regardless of flatness.