TradingView compatibility
-
Commission is booked once per order, not once per fill leg. A reversal executes as two fills — the closing leg and the opening leg — but TradingView charges it as a single order. Both legs now share one commission pool: the pool rounds the order total once and splits it back over the legs in quantity proportion, booking only the still-unbooked part.
strategy.commission.percent: the rate isprice * fxas a plain double, multiplied by the commission percentage in exact decimal arithmetic; the cash rounding lands on the double of that product. The order total israte * qty_total, whereqty_totalis the decimal sum of the leg quantities.strategy.commission.cash_per_order: the flat fee is charged once per order and split between the closing and the opening leg. When risk management suppresses the opening leg the closing leg carries the whole fee, so the direction check now runs before the pool is created.strategy.commission.cash_per_contract: unchanged per-contract rate, now booked through the same pool.
Cash rounding is half-up to ten significant digits on the double's shortest repr instead of onto a fixed
1e-6grid, and the per-leg split uses decimal division so exact half ties resolve upward as TradingView does. -
Realized gross P&L lands in
strategy.netprofitbefore the exit commission, matching TradingView's ordering. -
A reversal's remaining size is re-derived with the tolerant lot floor instead of by subtraction. The subtraction lost an ULP, which floored later percentage-based closes one lot low.
_size_roundnow returns positive zero for a zero lot count. -
A percent commission could be booked at another script's rate. The commission booking read the cached account-currency conversion rate on the invariant that the caller had just sampled it on this bar, but the new identity latch (see below) returns before refreshing it — so in a process running several scripts, a non-converting run could pick up a converting one's rate. The latch is now the authority at the read site too.
Performance
Issue #77 reported a strategy slowdown against v6.5.7. The framework itself had gotten faster over the same range (an empty indicator by 20%, eight ta.* series by 27%), which masked the strategy engine's own growth: the cost of a bar beyond that baseline had grown 47% holding one position and 31% while trading.
-
The account-currency memo is skipped entirely when nothing converts. Every money expression in the engine is
<something> * pointvalue, so all of those reads route through the account point value, whose memo was keyed on the bar — re-sampling the rate once per bar and re-checking a three-term key about 4.5 times per bar, even for runs that never convert. Every strategy that does not declarestrategy(currency=...)is such a run. An absent account currency now folds into the symbol's own up front, and a single structural comparison — the script's declared currency against the symbol's — latches the identity case, which returnssyminfo.pointvaluewith no memo read and no resample. The predicate is deliberately never the sampled rate: a genuine conversion can pass through a rate of exactly1.0on some bar, and latching on that would freeze the identity for the rest of the run. -
Two run invariants are hoisted out of the per-bar loop in
ScriptRunner.run_iter(the broker-mode flag and the simulator position cast), and the identity latch is inlined at the margin-call and bar-P&L finalization sites, which a bar with an open position enters up to five times between them. -
Seven per-bar sites in the simulator no longer walk containers that are empty whenever a position is merely held: the order-book generator was created and immediately exhausted, and the
list()snapshots existed only because a fill may mutate the dict they copy. An empty container has nothing to mutate, so it is neither copied nor walked.
Measured on 200000 bars with the revisions alternating: 1.1181 -> 0.9571 s holding one position and 1.4035 -> 1.2487 s trading actively, both now below v6.5.7 (0.9990 and 1.2779). Trades and netprofit are identical on every bench, and the PyneComp TradingView corpus passes 145/145.
Broker fixes
-
A fanned close now settles only when every leg has reported. The one-way emulator splits a close across several hedge legs, and each leg is a separate broker order with its own terminal event — so the first leg's FILL proves nothing about its siblings. The must-settle marker now tracks the dispatched Pine-unit quantity and the dispatched leg count, credits every booked slice exactly once (including slices deferred while the parent entry fill was still missing), and stays armed while either signal is short. A sibling rejected after the first leg filled therefore still escalates instead of being silently retired.
-
An off-grid fan no longer hangs on the stale grace. The dispatched quantity is a Pine-unit plan sum while the broker executes it snapped to its own integer volume grid, so the fills can total slightly less than the plan and the quantity check alone would never be satisfied. The leg-count signal completes the fan once every dispatched leg has reported a terminal FILL, regardless of the residual difference.
-
A fill the engine could not book keeps the marker armed. A slice with positive quantity but a missing or zero price is discarded by
record_fill, which leaves the local view short of the broker's real execution while every leg still reports terminally. Such drops are tracked by(fill_id, qty)and suppress the leg-count signal; because an unbookable fill's id is deliberately kept out of the seen-fill set, the broker can redeliver the same execution with a corrected price, and that redelivery discharges the drop and re-arms the completion signal. -
The stop-and-reverse fold's surplus corrective close is now restart-durable. Its must-settle marker was armed in memory only, so a restart while the correction was in flight — or parked with an unknown disposition — silently dropped the contract: a post-restart
rejectedresolution found no armed marker, skipped the escalation, and left the surplus exposure open. The marker is now persisted as append-only audit events, written before the dispatch so a crash inside the dispatch window cannot lose the record, with cumulative progress events for booked slices. Startup replay re-arms every armed-without-settle marker, and a new stale-grace reconcile probe proves settlement against the broker snapshot: it settles from the snapshot when that matches the corrected expectation, and halts for manual intervention when the correction is provably absent or the state is ambiguous. Replayed markers anchor on the last-armed marker's persisted arm-time pair, since the post-restart engine view is re-adopted from the very snapshot being probed. -
Duplicate fills cannot slip through across a restart. Settled markers seed the shared settled-defensive-close caches on both settle paths; per-slice order ids and client order ids ride the progress and settle payloads so a fanned correction's earlier children are covered; broker-native fill ids are reseeded for still-outstanding markers (safe mid-flight, because a fill id names one execution); and a settled-parent ring recognises the
{parent}:{leg}children the engine never observed, so a late leg arriving after settlement is dropped before it can be booked. -
A close that leaves a fan incomplete is no longer treated as neutralising its parent. Its fill quantity is deferred alongside the partial slices, so it is still booked and reduced once the parent entry fill arrives.
-
Startup recovery accounts for retired journal exposure. Run-ownership reconstruction now subtracts the closed quantity a plugin recorded on the entry row, clamped into the row's filled range, so the owned net reflects the venue's remaining exposure.
API
-
JOURNAL_EXPOSURE_RETIRED_EXTRA_KEY(pynecore.types.strategy) is the documentedOrderRow.extraskey for entry exposure already closed back on the venue. An entry row'sfilled_qtyis a monotone cumulative-execution watermark that a partial close must never decrement, so plugins whose close fills do not land as separate journal rows accumulate the closed quantity under this key instead. -
CloseFanResultgaineddispatched_qty— the Pine-unit quantity the dispatched legs cover, in the same unit asOrderEvent.fill_qty. The existinglegsvolumes are broker-grid integers (cTrader centi-units, Capital.com lot-step counts) and are not comparable with fill quantities;dispatched_qtyis what a caller compares against the fills that come back. It is a required field, so any code constructing aCloseFanResultdirectly must pass it.