Skip to content

Partial exits: three shapes where a partial strategy.exit gets no quantity, or one order for several same-ID entries (related to #694) #810

Open

Description

@letiep97

Summary

Three scripts where a partial strategy.exit (qty_percent or qty) closes a different quantity than on TradingView. In each, a control that differs in one place gives TradingView's trades on PineForge, and TradingView gives the control's trades for the script too (except case C, where TradingView's own list is shown). No diagnostic is raised.

# Shape PineForge
A Partial exit in ticks (profit) placed with the entry, then a full exit with a price stop for the same entry the partial exit gets no quantity; the full exit closes everything
B strategy.exit(qty = …) sent on every bar, then a full exit for the same entry the partial exit has no quantity from its third call on
C Two entries with the same ID (pyramiding = 2) and one strategy.exit(qty_percent = 50) from that ID one exit order, sized from the first entry; the second entry gets none

A is close to case 1 of #694 (tick and price levels mixed on exits placed with the entry), and the 2026-11-05 note there on "the handling of an absolute sibling when a relative exit is activated" may be the same path. C is close to case 3 (pyramiding, exit levels per entry), here for the quantity. They are filed here so they can be tracked apart from #694.

Versions

pineforge-engine v1.11.0 (a4472324) with pineforge-codegen 1.11.0, and v1.10.0 (59f98ba8) with Codegen 1.10.0: the same trade lists on both. Apple clang 17.0.0, macOS 15.8 arm64. BTCUSDT 1h bars from 2024-01-01 to 2024-03-01 (1,441 bars), initial capital 10000, no commission or slippage, mintick 0.01, qty_step 0.00001, input timeframe = script timeframe = 1h. Times are UTC bar times.

TradingView: BINANCE:BTCUSDT, 1h, Deep Backtesting over the same dates, measured on 2026-11-10. Trade counts include the last trade of the range, which TradingView leaves open and PineForge closes on the last bar; net profits are over the other, closed trades.

A. Tick partial exit placed with the entry, then a price exit

//@version=6
strategy("tick partial exit before a price exit", overlay = true, initial_capital = 10000, process_orders_on_close = true)
fast = ta.ema(close, 10)
slow = ta.ema(close, 30)
if ta.crossover(fast, slow)
    strategy.entry("Long", strategy.long, qty = 0.02)
    strategy.exit("Half", "Long", qty_percent = 50, profit = 50000)
    strategy.exit("Rest", "Long", stop = close - 1000)
if ta.crossunder(fast, slow)
    strategy.close("Long")

Control: limit = close + 500 instead of profit = 50000. With process_orders_on_close = true the entry fills at the signal bar's close, so both put Half at the same price.

Script Control
TradingView 43 trades, net profit 60.13: the control's list 43 trades
PineForge 26 trades: no entry split; Rest or strategy.close closes every 0.02 43 trades: 17 of 26 entries split 0.01 + 0.01; the 42 closed trades equal TradingView's

First entry, 2024-01-04 16:00 at 43915.71: the control closes 0.01 by Half on 01-04 19:00 at 44415.71 and 0.01 by Rest on 01-05 01:00 at 42915.71; the script closes 0.02 by Rest on 01-05 01:00.

What narrows it:

  • Without process_orders_on_close: the same, 26 trades. Both exits at top level on every bar (profit = 50000, and stop = ta.lowest(low, 20)), without process_orders_on_close: 26 trades, no split; TradingView 45 trades with the entries split.
  • The same two calls sent only while strategy.position_size > 0: 24 of 26 entries split. So the partial exit is lost when it is sent before the entry fills.
  • Rest with only a price limit: no split. Half with a price limit and Rest in ticks (loss = 100000): split. Both in ticks: split without process_orders_on_close (43 trades), not with it (26).
  • Rest with qty_percent = 50: the control's 43 trades.

B. qty partial exit sent on every bar

//@version=6
strategy("partial exit before another exit", overlay = true, initial_capital = 10000)
if ta.crossover(ta.ema(close, 10), ta.ema(close, 30))
    strategy.entry("Long", strategy.long, qty = 0.02)
if strategy.position_size > 0
    strategy.exit("Half", "Long", qty = 0.01, limit = strategy.position_avg_price * 1.01)
    strategy.exit("Rest", "Long", limit = strategy.position_avg_price * 1.02)
if ta.crossunder(ta.ema(close, 10), ta.ema(close, 30))
    strategy.close("Long")

Control: qty_percent = 50 instead of qty = 0.01. On a 0.02 entry the two are the same order.

Script Control
TradingView 41 trades, net profit 39.59: the control's list 41 trades
PineForge 34 trades: 8 of 26 entries split 41 trades: 15 of 26 split; TradingView's list

The 8 entries the script still splits are those whose Half limit is reached on one of the two bars after the fill bar. First entry that differs, 2024-01-06 17:00 at 43872.35: the control closes 0.01 by Half on 01-07 13:00 at 44311.08 and 0.01 by strategy.close on 01-08 01:00 at 43632.00; the script closes 0.02 by strategy.close.

What narrows it:

  • Exits sent only while bar_index - strategy.opentrades.entry_bar_index(0) <= 1 (two calls per entry): 41 trades. With <= 2 (three calls): 34.
  • Half limit out of reach (close * 2) until the call with index k (0 = the fill bar), then close * 0.9: Half fills for k = 0 and 1, never for 2, 3 or 4. With qty_percent = 50 it fills for each k tested, 0 to 3.
  • Without the Rest line, or with Rest given an explicit qty = 0.01: 41 trades.

C. Same-ID entries with a qty_percent exit

//@version=6
strategy("same-id entries with a partial exit", overlay = true, initial_capital = 10000, pyramiding = 2)
fast = ta.ema(close, 10)
slow = ta.ema(close, 30)
up = ta.crossover(fast, slow)
if up or up[2]
    strategy.entry("Long", strategy.long, qty = 0.02)
strategy.exit("TP", "Long", qty_percent = 50, limit = strategy.position_avg_price * 1.01)
if ta.crossunder(fast, slow)
    strategy.close("Long")

The Pine documentation (Strategies, "Exits for multiple entries") says such a call places a separate exit order for every entry with that ID, each sized from its own entry. Control: without qty_percent = 50.

Script Control
TradingView 68 trades, net profit 126.16 on the closed ones. Each take-profit fill with both entries open closes 0.02: 0.01 per entry, both booked against the first entry 52 trades, net profit 126.23
PineForge 68 trades, net profit 166.67. 16 take-profit fills: 13 close 0.01 (half of the first entry only), 3 close 0.03 (half of the first and all of the second) 52 trades, the same list as TradingView
Entries TradingView, script PineForge, script
2024-01-04 17:00 at 43915.70, 19:00 at 44048.95 0.02 on 01-04 19:00 at 44354.86 0.01 of the first and all 0.02 of the second on 01-04 19:00 at 44354.86
2024-01-06 17:00 at 43872.35, 19:00 at 43995.89 0.02 on 01-07 13:00 at 44373.47 0.01 of the first on 01-07 13:00; the rest (0.01 + 0.02) on 01-08 01:00 at 43632.00

Of the 47 exit times in the range, PineForge closes a different quantity than TradingView at 32. With the second entry one bar after the first (up or up[1]), PineForge closes 0.02 per fill, which matches only because the single order is then sized from the whole position; on fills where the second entry has just filled it closes 0.04.

Expected

The control's trade list for A and B, whatever the exit's level type or the number of calls. For C, one take-profit order per entry, each for 50% of its own entry, so that every fill with both entries open closes 0.02 in total.

Workaround

A: give the partial exit a price level, or send the exits once the position is open. B: use qty_percent, or give the full exit an explicit qty. C: none found that keeps one entry ID.

Activity

  1. luisleo526 commented on Nov 10, 2026

    @luisleo526
    Collaborator

    Thank you for the detailed report, the controls and the trade lists.

    We are reproducing all three shapes (A, B and C) on the released 1.11.0 and 1.10.0 builds. We are also checking them against a fix in progress in the same exit-sizing code, to see whether that fix changes them. We will post what we find here, including which releases are affected and how it relates to #694.

  2. luisleo526 commented on Nov 10, 2026

    @luisleo526
    Collaborator

    Confirmed: a silent wrong value in every release from 1.0.0 to 1.11.0. We measured all three shapes on the release builds and against TradingView (BINANCE:BTCUSDT 1h, 2024-01-01 to 2024-03-01). In every shape, the one-change control gives TradingView's trades row for row on PineForge, and the script itself does not:

    • A (a partial exit in ticks placed with the entry, then a price stop): TradingView 43 trades; PineForge matches 9 of them.
    • B (strategy.exit(qty = ...) sent on every bar): TradingView 41; PineForge matches 27.
    • C (pyramiding = 2, one qty_percent = 50 exit from the shared id): TradingView 68; PineForge matches 49.

    Workarounds, verified by output for A and B (the engine then equals TradingView row for row):

    • A: give the partial exit a price (limit = ...) instead of profit in ticks.
    • B: use qty_percent instead of an explicit qty on a re-sent exit.

    C has no verified workaround yet. The causes are found in the exit-reservation code. A fix is being prepared, and we will post here which release carries it.

  3. luisleo526 commented on Nov 13, 2026

    @luisleo526
    Collaborator

    Status 2026-11-13: the fix is in progress, with a candidate due 2026-11-14; target 1.13.0 (planned 2026-11-15) if it passes review and checks in time, otherwise the next patch release. We post the result here.

    🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions