Skip to content

v6.5.4 — TradingView strategy parity: big-money sizing, margin calls, trailing stops, Heikin Ashi security

Choose a tag to compare

@wallneradam wallneradam released this 04 Jul 15:27
· 439 commits to main since this release

PyneCore v6.5.4 continues the TradingView backtest-parity work: the strategy broker emulator now reproduces TradingView's large-equity sizing, margin-call and trailing-stop behaviour, its two-ledger FIFO trade attribution and its fill-time default sizing. It also adds Heikin Ashi support to request.security(), honours calc_bars_count, fixes builtin method dispatch on na and compiled-script builtin resolution, and removes a macOS request.security() latency bottleneck.

Features

Heikin Ashi in request.security()

request.security(ticker.heikinashi(symbol), tf, expr) now returns expr evaluated on Heikin Ashi candles instead of raising NotImplementedError. The chart-type helper marks the ticker with an internal separator; the security machinery strips it to the base symbol and transforms the feed to Heikin Ashi bars once up front, so the existing request.security() child consumes the transformed feed unchanged. On a higher-timeframe context the bars are aggregated to the period first and then transformed, matching TradingView, which builds Heikin Ashi from the requested timeframe's candles. Backtest (file-backed) only: live streaming and lower-timeframe chart-type requests raise a clear error, and renko / pointfigure / kagi / linebreak remain unsupported (path-dependent, need tick data).

calc_bars_count

The runner now honours calc_bars_count, restricting calculation to the last N chart bars. Bars before that window advance bar_index (kept absolute) but feed no series, run no main, process no orders and emit no output; series start fresh at the first calculated bar while last_bar_index is unchanged. A value of 0, or one covering the whole history, calculates every bar.

Strategy / backtest parity

Large-equity sizing, margin calls and trailing-stop timing

Extends the backtest broker emulator to reproduce TradingView's exact behaviour at large equity and on trailing exits (verified on the BINANCE:BTCUSDT 30m corpus with entries, exits and plots all matching TradingView):

  • Big-money entry sizing and margin gate: from 1e7 account-currency units of order money upward, TradingView re-judges the floor-sized quantity through a tick-grid margin gate instead of a plain float comparison. This gate is wired into entry(), order(), deferred-quantity resolution and the creation-time and fill-time margin checks (the fill-time check switches to an integer-tick comparison at ≥1e7 equity as well).
  • Margin-call trigger and cover at ≥1e7 equity: fires on an integer-tick comparison of truncated equity against the required margin rounded to a tick — half-up below 1e10 margin ticks, nearest multiple of ten ticks above — and sizes the cover from the resulting tick-shadow shortfall; the sub-1e10 float path is unchanged.
  • Close-time margin check: TradingView evaluates margin at every bar close and books the liquidation on that bar at the close price. The position is now trimmed on the correct bar and price instead of one bar late at the next open.
  • Reversal nofill keeps its closing leg: a market entry whose big-money sizing judges nofill no longer cancels a reversal outright — the opposite position still closes at the next open while the opening leg stays suppressed and re-issues a bar later.
  • Trailing-stop two-phase intrabar walk: the walk is split so the open tick and the legs up to the second extreme run before the intrabar margin-call checkpoints and the closing (extreme → close) leg resumes after them, matching TradingView's chronology when a partial margin call at the adverse extreme precedes the trailing fill.
  • Trailing re-issue semantics: a re-issue with changed trailing parameters is a cancel+replace that drops the carried water mark and re-arms from the issue bar's close, while an identical re-issue still inherits the ratcheted stop. A fractional trail_offset tick count is truncated to whole ticks the way TradingView does.

Fill-time sizing, margin cancellation and closing leg

  • Default-sized (percent_of_equity / cash) price-based entry and order quantities now resolve at the actual fill price instead of the placement close, matching TradingView. A marketable limit filling at the open therefore uses equity/open, not equity/limit. Market entries keep placement-close sizing (rejected at the next open if unaffordable), so only limit/stop orders defer.
  • The creation-time margin check now extends to price-based entries, plus a per-bar sweep that cancels pending entries whose required margin at the current price exceeds equity. This reproduces TradingView's asymmetry where a resting buy limit below the market never opens at 100% percent_of_equity sizing, while a resting sell limit above it survives.
  • The intrabar walk gains its closing leg (low → close and high → close) so an exit activated mid-bar by its own entry fill can fill on the same bar at its trigger price.

Two-ledger FIFO trade close

TradingView keeps two independent ledgers: closed trade rows are attributed FIFO across the whole position, while each exit/close order still settles against its own from_entry quantity. The simulator previously only closed per-entry, so pyramided closes split trade rows and cancelled brackets incorrectly. A per-entry open-quantity ledger now drives the exit-order lifecycle: a bracket survives its entry's rows being consumed FIFO by another entry's close and is cancelled only once its own bound quantity is spent; a from_entry-bound exit leg stays dormant until its entry fills (no triggering, no OCA-sibling cancellation, no fill-cap counting while the entry is pending, cancelled or rejected); and strategy.close(id) sizes off the bound entry's open quantity instead of the whole position.

Fixes

Builtin method dispatch on na

A builtin method invoked on an na builtin object (e.g. naLine.delete() on a line that was never drawn) now dispatches to that builtin's namespace, which treats na as a no-op the way Pine does. An na of a UDT or untyped value matches nothing and falls through to user-method dispatch.

Compiled-script builtin resolution (__ren__ images)

Two runtime paths matched user-source names verbatim and missed PyneComp's canonical __ren__ renamed images, regressing previously-working compiled scripts:

  • BuiltinShadowTransformer keyed the alias-shadows-builtin fallback on the emitted alias name, so a compiled import ... as ta__ren__ no longer matched and non-exported members (ta.crossover) raised AttributeError. The __ren__ suffix is now stripped from the alias, and an unserved member retries the builtin registry with the bare name.
  • The overload dispatcher did not adapt bare Pine keyword arguments to a callee's canonically renamed parameter (position vs position__ren__), raising "No matching implementation". Such kwargs are now renamed before selection.

Performance

request.security() on macOS

macOS has no sem_timedwait, so a timed multiprocessing wait falls back to CPython's sem_trywait + select() polling emulation, adding roughly 20 ms of wake latency per wait. The per-bar chart↔security handshake used a 0.5 s liveness-poll timeout on every wait, so that quantization dominated security-context runs (over 80% of wall time). Child-death detection is moved out of the wait onto a daemon thread that blocks on the process sentinel (no polling), letting the per-bar waits become plain untimed waits. Output is byte-identical; security-context runs are several times faster.