Skip to content

Releases: heyphat/piner

0.13.0 — persistent historical tail replay

Choose a tag to compare

@heyphat heyphat released this 22 Aug 02:11
60c22b8

Added

  • Persistent historical-tail execution — HistoricalTailRunner preserves
    Pine historical semantics while replaying safe final-bar revisions through
    Driver.replayHistoricalTail(). Structural changes, strategies,
    request.security, input/context changes, and unsupported cases remain
    conservative full-history fallbacks. Both synchronous and time-sliced APIs
    support cancellation and generation-safe publication.
  • Historical replay observability and benchmarking — exported replay result
    reasons, reusable historical checkpoints, and a focused tail benchmark make
    host scheduling decisions measurable without substituting realtime
    onTick() semantics.

Fixed

  • Historical checkpoints now clone var, varip, miscellaneous runtime state,
    and retained object-valued series history as one graph, preserving aliases
    and making repeated restore/replay safe.
  • Failed suffix execution transactionally restores the previously committed
    outputs, drawings, and runtime state before rethrowing the original error.
  • Historical run binding now matches Engine for validated mintick, Bar
    Magnifier timeframe mapping, and cloned injected magnifier datasets.
  • Bar and input comparisons preserve JavaScript numeric semantics: signed zero
    remains distinct, repeated NaN compares equal, and infinities are retained.
  • Tail truncation clears stale numeric/object series writes and output channels,
    including declarations introduced only by a discarded suffix.

0.12.0 — strategy metrics

Choose a tag to compare

@heyphat heyphat released this 09 Aug 09:41
dd0fcc4

Added

  • R-multiple trade analytics — planned-risk-normalized performance for
    hosts (a piner report extension, not a Pine builtin: TradingView's Strategy
    Tester has no equivalent).
    • The broker freezes each entry lot's initial risk at the first
      executable protective stop covering it (strategy.exit stop= or
      loss=), measured from the lot's fill price to the planned stop level —
      before gap/slippage fill adjustments. Initial risk is intent, not outcome:
      later stop moves, bracket re-submissions, and trailing ratchets never
      rewrite it, and the eventual fill price doesn't either.
    • Each ClosedTrade row gains initialRisk (the planned loss at that stop,
      prorated to the row's quantity; null when the entry lot never had a valid
      protective stop) and rMultiple (net row profit / initialRisk; null
      without risk data). Rows closed by market or forced closes inherit a
      lot-wide denominator only when a stop covered the whole remaining lot —
      partial stops risk only the rows they actually close. Trailing-only exits
      and stops at/beyond breakeven define no initial risk.
    • StrategyMetrics gains rMultipleExpectancy (mean rMultiple over rows
      with valid risk data; null when no row has any — never derived from
      hindsight values like max drawdown) and rMultipleTradeCount (rows
      included — surface it as n/N coverage so the selection is visible).
      Both new fields are required on the interface: additive for everyone who
      reads engine output, a type-level addition for code that constructs
      StrategyMetrics objects by hand.
    • Frozen risk participates in the realtime rollback contract: a speculative
      tick's stop fill (and its R row) is fully discarded when the open bar's
      final shape doesn't confirm it.
  • Annualized return decomposition — StrategyMetrics also gains
    arithmeticReturnPercent and geometricReturnPercent (both computed over
    the exact same finite per-bar return sample as Sharpe and both linearly
    annualized, not compounded) plus volatilityDragPercent, their exact AM-GM
    gap (clamped ≥ 0). Geometric return and the drag are NaN when any per-bar
    return falls below −100% (equity crossing zero between bars); a single
    −100% bar yields a defined geometric mean of −1. No TradingView
    counterparts. Like the R fields, these are required interface fields —
    additive for readers, a type-level addition for hand-built
    StrategyMetrics objects.

0.11.1 — byte-reproducible releases

Choose a tag to compare

@heyphat heyphat released this 28 Jul 12:37
34b691f

Changed

  • Byte-reproducible releases. The release and CI workflows now pin the
    exact Bun version instead of latest, so a published tarball can be
    binary-verified against a local build of the tagged tree (bun's bundler
    changes its CJS interop scaffolding between versions, which previously made
    dist/*.cjs differ across toolchains with identical sources). No functional
    changes to the engine — 0.11.1 is content-equivalent to 0.11.0, rebuilt
    under the pinned toolchain with the tag on main.

0.11.0 — use_bar_magnifier: historical LTF fill traversal

Choose a tag to compare

@heyphat heyphat released this 28 Jul 12:21
cba8d30

Added

  • use_bar_magnifier — historical lower-timeframe fill traversal
    (TradingView's Bar Magnifier Properties toggle). Declaring
    strategy(use_bar_magnifier = true) — or forcing it via
    EngineOptions.strategy.useBarMagnifier — makes the broker fill orders
    against real lower-timeframe OHLC on historical bars instead of the
    assumed one-bar intrabar path, so an entry and its bracket can resolve
    inside one chart bar in the order the market actually moved.
    • Host-injected data, engine stays pure: supply the LTF dataset via
      engine.ctx.magnifierData (per-sleeve:
      PortfolioSleeveSpec.magnifierData) as the new exported
      BarMagnifierData shape — target bars, explicit per-chart-bar close
      times, and a coverage contract. Validation fails closed: piner never
      sorts, repairs, or clamps injected data, and never fetches.
    • TradingView's chart→intrabar mapping, implemented once and exported
      (barMagnifierTimeframe(), parsePineTimeframe(),
      BAR_MAGNIFIER_CONTRACT_VERSION = 1, BAR_MAGNIFIER_MAPPING_VERSION = 3)
      with the published unbounded terminal rows: month, multi-week, and >7D
      charts map to 1D; >=1000T maps to 100T. TradingView's
      200,000-target-bar limit is enforced as broker semantics (envelope
      filter first, then the newest 200k), and uncovered chart bars keep the
      standard chart-OHLC assumption.
    • Scope — proxy-validated partial support, not TV parity: historical
      no-COOF bars only. calc_on_order_fills keeps its audited chart
      scheduler, realtime updates fall back per tick, and empty or
      cap-intersected buckets fall back per bar. Semantics validated against
      TradingView's public documentation and a deterministic 352-run
      differential matrix vs an independent C++ Pine engine
      (test/fixtures/bar-magnifier/); no continuous segment is ever invented
      across an LTF row boundary, and fill timestamps are the sub-bar's open
      time. See docs/pine-semantics.md §9 and docs/architecture.md §6.1.
    • Optional StrategyReport.barMagnifier block reports effective state
      (active, magnifiedBars, intrabarsUsed, capFallbackBars,
      dataFallbackBars, coverage). Present only when requested — flag-off
      serialized reports are byte-identical to 0.10.0. Portfolio reports carry
      per-sleeve blocks plus an aggregate.

Fixed

  • Stop-limit entries whose limit is beyond the stop no longer fill at
    untraded prices.
    A just-armed stop-limit filled at its limit level
    whenever the bar's extreme crossed it — with stop = 100, limit = 101 on a
    bar with high 100.5 the fill was 101, above the bar's high; a gap-open past
    the stop also filled at prices the bar never traded. Per the documented
    emulator rules (fills confined to the bar's range; a marketable limit fills
    without waiting; gap fills use the market coordinate, not the level), a
    limit that is marketable the moment its stop trips now fills at the
    activation price — max(open, stop) for longs, mirrored for shorts. The
    retrace configuration (limit on the near side of the stop, as in
    TradingView's own stop-limit demo) is unchanged. Backtests using
    far-side stop-limit entries produce different — correct — fills than
    0.10.0.

0.10.0 — calc_on_order_fills: TV-parity intrabar recalculation

Choose a tag to compare

@heyphat heyphat released this 26 Jul 02:40
becacab

Added

  • calc_on_order_fills — historical intrabar recalculation (TradingView's
    "After order is filled" Properties checkbox). Declaring
    strategy(calc_on_order_fills = true) — or forcing it via
    EngineOptions.strategy.calcOnOrderFills, which overrides the header either
    way — re-executes a strategy after every broker fill on historical bars,
    so orders placed by a post-fill execution (brackets, pyramids, reversals)
    can fill on the same bar.
    • Implements the empirically pinned path-point model: four fill points
      per bar — the open twice (arrival + walk start), then the extremes
      nearer-first; the close is not a fill point (leftovers carry to the
      next bar's open); an order born mid-bar fills discretely at the next
      point's price
      , then continuously at its own level. Derived from and
      verified against a real TradingView List-of-Trades export
      (BINANCE:XAUUSDT.P 1h): all 55 trades fill-for-fill and all 110
      per-trade favorable/adverse excursion values
      , replayed network-free on
      both backends (test/calc-on-order-fills-tv-parity.test.ts).
    • Account state is marked chronologically (every traversed path point,
      fill price, and the close — never an end-of-bar full-range replay):
      per-trade MFE/MAE is TV-pinned; margin evaluates per pass over the
      current exposure interval (restarted by every fill, so a late entry
      or add can never be liquidated at a price it never traversed, and a
      deficiency is margin-called before a same-bar exit erases it);
      equity-risk rules halt at the pass where they breach, force-closing at
      the very next path point; a forced liquidation triggers its own
      recalculation. Rollback between same-bar executions matches the
      realtime model: series/var/ta/drawings restore, varip and the broker
      persist. Account-level margin/risk timing is an engineering default
      (chronologically sound, not yet TV-evidenced) — see
      docs/strategy-broker.md §12 and docs/pine-semantics.md §1.
    • process_orders_on_close composes: a close-pass fill triggers one more
      execution whose orders carry to the next bar.
  • calc_on_every_tick parsed into StrategySettings.calcOnEveryTick as a
    deliberate no-op: on TradingView it changes realtime behavior only —
    historical backtests are identical either way, so this is TV-correct for
    every backtesting host.

Changed

  • Realtime: the process_orders_on_close pass now runs only on a bar's
    closing update instead of speculatively on every tick. Committed
    results are unchanged (speculative fills were always rolled back); mid-bar
    realtime read-backs no longer show transient close fills.

Flag-off behavior is byte-for-byte unchanged: strategies that do not declare
calc_on_order_fills produce identical results to 0.9.0.

0.9.0 — TV-verified margin-call model + lot-step quantity truncation

Choose a tag to compare

@heyphat heyphat released this 15 Jul 14:05
c62b4db

Changed (breaking)

Backtest results change for any margin-enabled or derived-quantity strategy
(the API is additive — one new optional setting). All three changes move the
broker onto TradingView's empirically verified behavior, validated against a
42-event TV v6 margin-call ledger (BINANCE:XAUUSDT.P 15m; every event's bar,
fill price, and liquidated quantity reproduced exactly, and the full 85-row
trade list replayed within one 0.001 lot step).

  • Margin-call simulation matches TradingView's broker emulator.
    • Evaluation and fill happen once per bar at the bar's worst price for
      the position (low for longs, high for shorts) — previously the broker
      walked the 4-point intrabar path and could liquidate several times per
      bar at intermediate points.
    • The liquidated quantity follows TV's 10-step algorithm: the cover
      quantity is truncated to the symbol's minimum contract size (new
      StrategySettings.minQty, default 0.001, host-configurable) before the
      ×4 multiplier; when truncation yields zero the emulator liquidates
      exactly one unit (empirical TV behavior, undocumented by TV).
    • strategy.margin_liquidation_price rounds down to tick for longs, up
      for shorts
      (per the Help Center leverage article) instead of to the
      nearest tick.
  • Derived order quantities truncate to the lot step. strategy.cash and
    strategy.percent_of_equity sizing now truncate the computed quantity to
    minQty, as TradingView does (equity 10067.60 @ 4740 → qty 2.123, not
    2.12396). Explicit and fixed quantities pass through untouched. Set
    minQty: 0 to disable the truncation entirely.

Added

  • StrategySettings.minQty — the symbol's minimum contract size (lot
    step), the truncation unit for margin-call liquidations and derived order
    sizing. Exchange metadata, not a strategy() parameter: hosts configure it
    per symbol via broker.configure({ minQty }) or the EngineOptions.strategy
    override. Default 0.001.
  • PortfolioSleeveSpec.minQty — per-sleeve lot step for
    PortfolioEngine, merged into each sleeve's strategy override.

0.8.1 — TV operator parity fixes

Choose a tag to compare

@heyphat heyphat released this 14 Jul 12:35
224e69e

Fixed

  • String += is concatenation. Compound assignment on a string target
    (e.g. barTxt += "█") lowered to numeric addition, producing na — and
    drawing text built that way serialized as null, which could crash a host
    renderer. += now follows the same rule as binary + (string operand →
    concatenation) in both backends, per the v6 reference for +=.
  • == / != round float operands to nine fractional digits (v6 reference
    for ==/!=), so 0.1 + 0.2 == 0.3 is true as on TradingView. switch
    subject matching inherits the rule (it compiles to ==). Relational
    < <= > >= are unchanged, per the docs.
  • [] floors a float offset (close[2.9] reads close[2]) instead of
    indexing the history column with a non-integer, which returned undefined;
    a non-finite (na) offset now reads na.
  • ±Infinity is falsy in conditions, matching the ?: reference ("0 and
    also NaN, +Infinity, -Infinity" are false).

Full Changelog: v0.8.0...v0.8.1

0.8.0 — portfolio engine + request.security any-timeframe

Choose a tag to compare

@heyphat heyphat released this 11 Jul 03:03
fe20ce3

Added

  • PortfolioEngine — run one compiled strategy across a basket of
    symbols/timeframes under a single capital model and get a portfolio-level
    report (docs/portfolio-semantics.md). Two capital modes: isolated
    (each sleeve funded wᵢ·P from a total pot P, with per-sleeve weights)
    and shared (all sleeves draw on one pot on a merged master clock). Exposes
    PortfolioEngineOptions, PortfolioSleeveSpec, PortfolioSleeveResult, and
    PortfolioReport; the portfolio report reuses the StrategyReport shape so
    computeStrategyMetrics consumes it unchanged.
  • Engine.prepare() / Engine.step() — the external-clock half of run():
    bind a run's identity and full bar array without executing, then drive bars
    one at a time. Stepped bars keep exact historical semantics
    (barstate.ishistory / islast, last_bar_index) bit-for-bit with run(),
    with none of the realtime rollback overhead. Lets a host interleave several
    engines on one shared clock (used by PortfolioEngine).
  • EngineOptions.strategy — host override of strategy() header settings
    (initial_capital, commission, slippage, margin, …), applied after the header
    so overrides win. The funding primitive behind weighted/portfolio runs and
    commission/slippage sensitivity tests, without editing the source. Ignored for
    compiled non-strategy scripts.
  • Account exported from the strategy runtime — the funding account a broker
    draws on, extracted so several brokers can share one pot in a portfolio run
    while a lone broker keeps today's per-broker behavior bit-for-bit.
  • Same-symbol request.security resolves from injected real bars on any
    timeframe.
    When the host injects the requested timeframe's actual bars under
    securityBars["<symbol>@<tf>"], a same-symbol request.security now resolves
    against that real series — aligned by bar close time — instead of resampling
    the chart's own bars. This is required for a timeframe finer than the chart
    (which resampling cannot produce, so it silently degraded to the chart series)
    and is accurate for a higher one (resampling surfaced a just-closed
    higher-timeframe bar one chart-bar late). lookahead_off still holds: a chart
    bar sees only bars that closed by its own close, so a higher-timeframe value
    lands on the last chart bar of its period. With no injected series the
    request falls back to resampling the chart bars (unchanged). Verified bar-for-bar
    against TradingView on real 1D/2H/1H data.

0.7.0 — Margin simulation (margin_long/margin_short)

Choose a tag to compare

@heyphat heyphat released this 08 Jul 14:17
46be87f

Added

  • Margin simulation (margin_long / margin_short), modeled from
    TradingView's public margin documentation (design record:
    dev-docs/margin-plan.md; broker doc: docs/strategy-broker.md §9):
    • Order gating — an exposure-increasing fill whose resulting position
      would require more equity than is available at the fill price is rejected
      outright (never reduced), per the v6 migration guide. Closes, exit
      brackets, and the reducing leg of a netting order are never gated.
    • Margin calls — walking the bar's assumed intrabar path, the broker
      force-closes 4× the quantity needed to cover a margin deficit (TV's rule
      for "preventing constant Margin Call events"), capped at the whole
      position, booked through the normal FIFO close path. Not a halt: pending
      orders and exit brackets survive.
    • strategy.margin_liquidation_price now reports the live liquidation
      level (was a hardcoded na): na while flat, when the side's margin is 0,
      or for a fully-funded long.
    • StrategyReport.marginCalls — TradingView's "Margin Calls" Performance
      Summary count.

Changed

  • BREAKING (behavior): margin defaults are now the Pine v6 100/100
    (was: no margin model, i.e. the v5 0/0). Strategies that open positions
    larger than their equity — or shorts that lose enough — now get rejected
    entries and margin-call liquidations, exactly as on TradingView v6. To keep
    the old unlimited-buying-power behavior, declare
    strategy(..., margin_long = 0, margin_short = 0) (the migration guide's
    own escape hatch).

0.6.0 — TradingView strategy report metrics parity

Choose a tag to compare

@heyphat heyphat released this 06 Jul 13:00
9759480

Added

  • computeStrategyMetrics gains the rest of TradingView's Strategy Report
    ratios
    , closing most of the gap documented in
    docs/tradingview-strategy-report-metrics.md: avgWinLossRatio,
    largestWinPercentOfGrossProfit, largestLossPercentOfGrossLoss,
    netProfitPercentOfLargestLoss, avgBarsInWinners/avgBarsInLosers,
    returnOnInitialCapitalPercent, buyHoldPnL, and
    maxRunupPercentOfInitialCapital/maxDrawdownPercentOfInitialCapital.
  • Close-to-close run-up/drawdown family: maxRunupCloseToClose,
    maxDrawdownCloseToClose, avgRunupCloseToClose, avgDrawdownCloseToClose,
    avgRunupDurationDays, avgDrawdownDurationDays — computed from the
    bar-close equity curve, distinct from the existing intrabar max_runup/
    max_drawdown Pine builtins.
  • StrategyReport now also reports maxRunup/maxRunupPercent (the broker
    already tracked them; only maxDrawdown was previously exposed).

Changed

  • buyHoldReturnPercent/buyHoldPnL/outperformance now enter at the
    first closed trade's entry fill, matching TradingView's stated basis
    ("from when the strategy's first position was opened"), instead of always
    the second bar's open. Runs with no closed trades keep the old fallback.