Repository navigation
Releases: heyphat/piner
Releases · heyphat/piner
Release list
0.13.0 — persistent historical tail replay
Added
- Persistent historical-tail execution —
HistoricalTailRunnerpreserves
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
Enginefor validatedmintick, Bar
Magnifier timeframe mapping, and cloned injected magnifier datasets. - Bar and input comparisons preserve JavaScript numeric semantics: signed zero
remains distinct, repeatedNaNcompares 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
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.exitstop=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
ClosedTraderow gainsinitialRisk(the planned loss at that stop,
prorated to the row's quantity;nullwhen the entry lot never had a valid
protective stop) andrMultiple(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. StrategyMetricsgainsrMultipleExpectancy(meanrMultipleover rows
with valid risk data;nullwhen no row has any — never derived from
hindsight values like max drawdown) andrMultipleTradeCount(rows
included — surface it asn/Ncoverage 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
StrategyMetricsobjects 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.
- The broker freezes each entry lot's initial risk at the first
- Annualized return decomposition —
StrategyMetricsalso gains
arithmeticReturnPercentandgeometricReturnPercent(both computed over
the exact same finite per-bar return sample as Sharpe and both linearly
annualized, not compounded) plusvolatilityDragPercent, their exact AM-GM
gap (clamped ≥ 0). Geometric return and the drag areNaNwhen 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
StrategyMetricsobjects.
0.11.1 — byte-reproducible releases
Changed
- Byte-reproducible releases. The release and CI workflows now pin the
exact Bun version instead oflatest, 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/*.cjsdiffer across toolchains with identical sources). No functional
changes to the engine —0.11.1is content-equivalent to0.11.0, rebuilt
under the pinned toolchain with the tag onmain.
0.11.0 — use_bar_magnifier: historical LTF fill traversal
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
BarMagnifierDatashape — 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 to1D;>=1000Tmaps to100T. 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_fillskeeps 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. Seedocs/pine-semantics.md§9 anddocs/architecture.md§6.1. - Optional
StrategyReport.barMagnifierblock 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.
- Host-injected data, engine stays pure: supply the LTF dataset via
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 — withstop = 100, limit = 101on 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
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,varipand the broker
persist. Account-level margin/risk timing is an engineering default
(chronologically sound, not yet TV-evidenced) — see
docs/strategy-broker.md§12 anddocs/pine-semantics.md§1. process_orders_on_closecomposes: a close-pass fill triggers one more
execution whose orders carry to the next bar.
- Implements the empirically pinned path-point model: four fill points
calc_on_every_tickparsed intoStrategySettings.calcOnEveryTickas 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_closepass 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
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_pricerounds down to tick for longs, up
for shorts (per the Help Center leverage article) instead of to the
nearest tick.
- Evaluation and fill happen once per bar at the bar's worst price for
- Derived order quantities truncate to the lot step.
strategy.cashand
strategy.percent_of_equitysizing 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: 0to 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 astrategy()parameter: hosts configure it
per symbol viabroker.configure({ minQty })or theEngineOptions.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
Fixed
- String
+=is concatenation. Compound assignment on a string target
(e.g.barTxt += "█") lowered to numeric addition, producingna— and
drawingtextbuilt that way serialized asnull, 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==/!=), so0.1 + 0.2 == 0.3istrueas on TradingView.switch
subject matching inherits the rule (it compiles to==). Relational
< <= > >=are unchanged, per the docs.[]floors a float offset (close[2.9]readsclose[2]) instead of
indexing the history column with a non-integer, which returnedundefined;
a non-finite (na) offset now readsna.±Infinityis 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
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 fundedwᵢ·Pfrom a total potP, with per-sleeveweights)
andshared(all sleeves draw on one pot on a merged master clock). Exposes
PortfolioEngineOptions,PortfolioSleeveSpec,PortfolioSleeveResult, and
PortfolioReport; the portfolio report reuses theStrategyReportshape so
computeStrategyMetricsconsumes it unchanged.Engine.prepare()/Engine.step()— the external-clock half ofrun():
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 withrun(),
with none of the realtime rollback overhead. Lets a host interleave several
engines on one shared clock (used byPortfolioEngine).EngineOptions.strategy— host override ofstrategy()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.Accountexported 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.securityresolves from injected real bars on any
timeframe. When the host injects the requested timeframe's actual bars under
securityBars["<symbol>@<tf>"], a same-symbolrequest.securitynow 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_offstill 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)
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_pricenow reports the live liquidation
level (was a hardcodedna): 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.
- Order gating — an exposure-increasing fill whose resulting position
Changed
- BREAKING (behavior): margin defaults are now the Pine v6
100/100
(was: no margin model, i.e. the v50/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
Added
computeStrategyMetricsgains 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 intrabarmax_runup/
max_drawdownPine builtins. StrategyReportnow also reportsmaxRunup/maxRunupPercent(the broker
already tracked them; onlymaxDrawdownwas previously exposed).
Changed
buyHoldReturnPercent/buyHoldPnL/outperformancenow 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.