Skip to content

process_orders_on_close = true: strategy.close before a reversing strategy.entry sizes the new entry's qty_percent exit from the old position, and part of the new position gets no exit #803

Closed

Description

@letiep97

Summary

A script closes the opposite position with strategy.close() and, in the same execution, calls strategy.entry() in the new direction, then a partial exit (qty_percent = 50) and a full-size stop exit for that entry. With process_orders_on_close = true the close and the entry fill on that bar's close.

On TradingView the strategy.close() line changes nothing: with or without it the new position is qty, the partial exit covers half of it and the stop exit covers the rest. PineForge gives that result only without the strategy.close() line. With it:

  • the partial exit is sized from the position that was just closed, not from the new one;
  • when the old position was smaller than the new one, a part of the new position equal to the old position's size is covered by neither exit. It passes its stop and stays open until the next reversal.

No diagnostic mentions it.

Versions

pineforge-engine v1.11.0 (a4472324) with pineforge-codegen 1.11.0, and v1.10.0 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 and mincontract 0.00001, input timeframe = script timeframe = 1h. Trade lists come from the run report.

TradingView: BINANCE:BTCUSDT, 1h, Deep Backtesting over the same dates, measured on 2026-11-10.

Reproduction

//@version=6
strategy("close before a reversing entry", overlay = true, initial_capital = 10000, process_orders_on_close = true)
fast = ta.ema(close, 10)
slow = ta.ema(close, 30)
qty = strategy.initial_capital / 10 / close
if ta.crossover(fast, slow)
    strategy.close("Short")
    strategy.entry("Long", strategy.long, qty = qty)
    strategy.exit("TP", "Long", qty_percent = 50, limit = close * 1.02, stop = close * 0.97)
    strategy.exit("SL", "Long", stop = close * 0.97)
if ta.crossunder(fast, slow)
    strategy.close("Long")
    strategy.entry("Short", strategy.short, qty = qty)
    strategy.exit("TP", "Short", qty_percent = 50, limit = close * 0.98, stop = close * 1.03)
    strategy.exit("SL", "Short", stop = close * 1.03)

Control: the same script without the two strategy.close() lines.

Generated calls for the long side (Codegen 1.11.0), in this order:

strategy_close(std::string("Short"), "", na<double>(), na<double>(), false, 30064771091ULL);
strategy_entry(std::string("Long"), true, na<double>(), na<double>(), qty, "", "", 0, -1);
strategy_exit(std::string("TP"), std::string("Long"), (current_bar_.close * 1.02), (current_bar_.close * 0.97), na<double>(), na<double>(), na<double>(), 50, "", na<double>(), "", na<double>(), na<double>());
strategy_exit(std::string("SL"), std::string("Long"), na<double>(), (current_bar_.close * 0.97), na<double>(), na<double>(), na<double>(), 100.0, "", na<double>(), "", na<double>(), na<double>());

Result

TradingView gives the same 70 trades for both scripts (69 closed, one open at the end of the range). PineForge gives 70 trades for the control, equal to TradingView's in all 69 closed trades (quantity, entry and exit time and price), and 71 trades for the script with the strategy.close() lines, different from the 10th trade on.

The first three entries that differ (times are UTC bar times):

Entry TradingView (both scripts) and PineForge control PineForge with the strategy.close() lines
2024-01-08 11:00, Long 0.02240 at 44635.99 0.01120 closed by TP at 45528.71; 0.01120 closed by the next reversal 0.01145 closed by TP; 0.01095 closed by the next reversal
2024-01-10 02:00, Short 0.02175 at 45971.09 0.01087 closed by TP at 45051.66; 0.01088 closed by the next reversal 0.00540 closed by TP; 0.01635 closed by the next reversal
2024-01-10 19:00, Long 0.02151 at 46489.18 0.01075 and 0.01076, both stopped at 45094.50 on the next bar 0.00258 and 0.00258 stopped at 45094.50; 0.01635 not stopped, closed by the next reversal on 2024-01-12 01:00 at 46135.48

The PineForge quantities follow the position that strategy.close() closed on that bar:

  • 01-08 11:00: the short being closed is 0.02291, and TP takes 0.01145, half of it.
  • 01-10 02:00: the long being closed is 0.01095 (its TP half had filled), and TP takes 0.00540, half of 0.02175 − 0.01095.
  • 01-10 19:00: the short being closed is 0.01635, TP and SL together cover 0.00516 = 0.02151 − 0.01635, and the uncovered part is 0.01635.

What narrows it (PineForge v1.11.0)

  • Without process_orders_on_close = true, both scripts give the same trade list.
  • With qty_percent = 100 on TP, or with only the SL exit, both scripts give the same trade list.
  • With only the TP exit (no SL line) they still differ: for the entry on 2024-01-04 16:00 the control's TP exit closes 0.01138, and with the strategy.close() lines it closes 0.00554.
  • With the two exits called on the following bars, while the position is open, instead of with the entry, both scripts give the same trade list.
  • Commission plays no role: the difference is the same with and without one.

Expected

The trade list of the control, which is what TradingView gives for both scripts.

Workaround

Drop the strategy.close() call and let strategy.entry() reverse the position.

Activity

  1. luisleo526 commented on Nov 10, 2026

    @luisleo526
    Collaborator

    Thank you for the report and the script. It is received and in triage: we are reproducing it on your script and comparing against TradingView. I will post the result here, confirmed or not and with the planned fix version, by 2026-11-11 12:00 (Taipei time). If it is confirmed as a wrong value that ships in a released version, the release notes will say so with a workaround.

  2. luisleo526 commented on Nov 10, 2026

    @luisleo526
    Collaborator

    Result (2026-11-10): confirmed.

    Trigger: a strategy with process_orders_on_close = true that calls strategy.close() for the open position and, in the same execution, strategy.entry() in the opposite direction, then strategy.exit() with qty_percent below 100 for that entry. Affected versions: reproduced on 1.11.0; the reporter sees the same trade lists on 1.10.0. Effect: the partial exit is sized from the position that strategy.close() has just closed instead of from the new entry. When the old position was smaller than the new one, a part of the new position equal to the old size is covered by neither exit: it passes its stop and stays open until the next reversal. In the reporter's example PineForge gives 71 trades where TradingView gives 70, from the 10th trade on. Workaround: remove the strategy.close() call and let strategy.entry() reverse the position; with that form PineForge gives the 70 trades that TradingView gives (checked on the reporter's script and bars). Plan: a fix is being prepared for the next patch release; if it does not pass the release checks in time it moves to the following one and this difference stays listed.

  3. luisleo526 commented on Nov 10, 2026

    @luisleo526
    Collaborator

    Update (2026-11-10): while checking this report we found more shapes of the same kind. We ran each of them through the shipped 1.11.0 pair (engine 1.11.0 with Codegen 1.11.0) and compared the result with TradingView's own list of trades for the same script (NASDAQ:AAPL, 15-minute bars); they behave the same on current main; earlier releases were not tested for these shapes. All of them use process_orders_on_close = true, a position of 3 units that is reversed or replaced on a later bar by an entry of 2 units, and two exits for the new entry placed on the bar of that entry: strategy.exit("TP", id, qty_percent = 50, limit = ..., stop = ...) and strategy.exit("SL", id, stop = ...). In the reported case and in shapes (1) to (3) TradingView exits one unit of the new position at the limit and one unit at the stop; the engine differs as follows. (1) strategy(..., close_entries_rule = "ANY") with a reversing entry, with or without a strategy.close before it: the first exit takes the whole new position at its limit and the stop exit never fires. (2) strategy.close("A") followed in the same execution by a same-direction strategy.entry("B", ...) with exits for "B": the new position gets no exit and stays open. (3) A partial strategy.close("L", qty = 1) followed in the same execution by a reversing strategy.entry: TradingView closes the other two units with the entry and opens the new position; the engine never opens it. (4) The new entry reuses the id of the position it replaces (strategy.entry("X", strategy.long) and later strategy.entry("X", strategy.short)): TradingView gives the new position no take-profit fill and no stop fill, with or without a strategy.close("X") before the entry; the engine does the same with the close, but without the close its stop exit takes the whole new position. Writing the same strategy with a different id for each direction, the default close_entries_rule and no strategy.close before the reversing entry gives the same trades in the engine as in TradingView (a take-profit of half the new position and a stop for the rest; this is also the workaround for the case reported here); for shape (2), for shape (4) and for strategies that need close_entries_rule = "ANY" with several entries per direction we have no verified workaround. We plan to ship the fixes in 1.11.1 together with the reported case.

  4. luisleo526 commented on Nov 10, 2026

    @luisleo526
    Collaborator

    Update (2026-11-10): a second difference of the same kind, in the default configuration, found while checking this report. It needs a strategy that leaves process_orders_on_close at its default (false), holds a position of one size, and reverses it with a strategy.entry in the opposite direction with an explicit quantity that differs from the size of the position being replaced, and that places the exits of the new entry on the same bar: strategy.exit("TP", id, qty_percent = 50, limit = ..., stop = ...) together with strategy.exit("SL", id, stop = ...), with or without a strategy.close of the old position before the entry. PineForge takes the percentage from the size of the old position (capped at the new entry); TradingView takes it from the new entry. On NASDAQ:AAPL 15-minute bars: a long of 4 reversed into a short of 2 gives, on TradingView, 1 closed at the limit and 1 at the stop, and in PineForge the whole 2 closed at the limit and the stop never fires; a long of 2 reversed into a short of 4 gives 2 and 2 on TradingView and 1 and 3 in PineForge. A second shape on the same reversal, measured without a strategy.close: if the take-profit is given as an absolute quantity (strategy.exit("TP", id, qty = ..., limit = ..., stop = ...)), TradingView closes that quantity at the limit and the rest at the stop (qty = 1: 4 reversed into 2 gives 1 and 1; qty = 2: 2 reversed into 4 gives 2 and 2), while in PineForge the take-profit never fires and the stop closes the whole new position. We ran these examples on every published release image from 1.0.0 to 1.11.0 (1.0.0, 1.0.1, 1.1.0, 1.2.0, 1.3.0, 1.4.0, 1.5.0, 1.6.0, 1.6.3, 1.7.0, 1.8.0, 1.9.0, 1.9.1, 1.10.0 and 1.11.0; 1.6.1 and 1.6.2 were not tested) and all of them show both differences, so they are not recent regressions. A reversal whose new quantity equals the size of the position it replaces is not affected. Two ways of writing the strategy gave TradingView's trades in all fifteen releases on these bars: giving the reversing entry the same quantity as the position being replaced, or placing the strategy.exit calls in the first execution after the entry has filled (the fill bar's own execution, when strategy.position_size has already changed sign), using the limit and stop levels remembered from the signal bar (the exits are then live from the next bar); an absolute quantity is not a workaround. We measured one symbol and timeframe, one day, fixed quantities, no commission and a percentage of 50; other percentages, symbols, timeframes, default-quantity reversals and pyramided positions are not measured. The fix is planned for 1.11.1.

  5. luisleo526 commented on Nov 10, 2026

    @luisleo526
    Collaborator

    Hosted PineForge MCP: status for this issue

    The hosted PineForge MCP currently runs engine 1.10.0, which is affected by both differences described in the engine note above: #803 (comment)

    What you may see. Both cases need a strategy that keeps process_orders_on_close at its default (false). It reverses a position with a strategy.entry in the opposite direction whose quantity differs from the position it replaces, and places the new entry's exits on the same bar.

    • A percent take-profit (strategy.exit(..., qty_percent = ...)): the percentage is taken from the size of the OLD position instead of the new entry, so the exits close the wrong quantities.
    • An absolute-quantity take-profit (strategy.exit(..., qty = ...)): the take-profit never fires, and the stop closes the whole new position.

    A reversal whose new quantity equals the size of the position it replaces is not affected.

    How to check a script. Run check_tradingview_parity on it with your data. A difference in the exits after a reversal is this issue.

    Workarounds (verified on every published release from 1.0.0 to 1.11.0, on the engine note's test bars):

    • give the reversing entry the same quantity as the position it replaces; or
    • place the strategy.exit calls on the bar after the entry has filled (once strategy.position_size has changed sign), using the limit and stop levels remembered from the signal bar. The exits are then live from the bar after the fill.

    Using an absolute quantity instead of a percentage is not a workaround.

    Fix. The fix is planned for engine 1.11.1. The hosted MCP moves to 1.11.1 after that release is out and has passed our checks. We will post here when it is live.

  6. luisleo526 commented on Nov 11, 2026

    @luisleo526
    Collaborator

    Status update: the first part of the fix is now on the engine main branch in 360c4ab (#825). It includes #817.

    This issue stays open. The remaining cases are part of the next batch of engine fixes, and we will post here when they are merged.

  7. luisleo526 commented on Nov 12, 2026

    @luisleo526
    Collaborator

    Fixed in PineForge Engine 1.12.0. With process_orders_on_close and the default FIFO close rule, the named strategy.exit calls of a reversing strategy.entry with an explicit quantity, placed after a strategy.close of the old position, are now sized from the new entry (see the 1.12.0 changelog).

    🤖 Generated with Claude Code

  8. letiep97 commented on Nov 13, 2026

    @letiep97
    Author

    Confirmed fixed in 1.12.0, thank you.

    Checked on the 1.12.0 release (engine v1.12.0 6e27d0f2, pineforge-codegen 1.12.0), built from source on macOS 15.8 arm64, with the scripts from this issue (BTCUSDT 1h, 2024-01-01 to 2024-03-01, capital 10000, no commission).

    • The reversal script with strategy.close before the reversing strategy.entry and a partial strategy.exit now gives 70 trades, the same list as its control and as TradingView (1.11.0 gave 71, with a leftover piece of the new position).
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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions