In my last post I proved my own post #2 was wrong: the "ATR bracket" in that RSI strategy was never an ATR bracket. strategy.exit's profit and loss are distances in ticks, not price points, so on the data I ran, the bracket was about 0.00016 points wide and every trade exited one bar after entry. The 10-bar time exit was dead code — it fired zero times in 60 trades.

This post applies the fix that post promised: same strategy, same data, same parameters, but the exit rewritten with price levels — limit and stop computed from the entry price. The time exit finally fires. And the strategy loses more money. Both of those are true, and the gap between them is the whole point.

The headline numbers first, because this is the part you can check:

#4: tick bracket (profit/loss) #5: price bracket (limit/stop) Buy & hold, same period
Trades (closed) 60 58 (+1 still open) —
Net result -$1,353.29 (-13.53%) -$2,572.67 (-25.73%) +14.68%
Win rate 27 / 60 (45%) 19 / 58 (33%) —
Profit factor 0.695 0.587 —
Avg bars held 1.0 3.6 —
Time exits (10-bar rule) 0 6 —

Same Pine, same 4,089 bars of CSI 300 daily data (2010-01-04 to 2026-11-06), same $10,000. The only change is one line. The rest of this post is the autopsy: what the fix actually changed, the exact bar where the time exit first fires, and why a correct fix made the numbers worse.

The fix: one line

The script is the RSI mean-reversion strategy from post #2, unchanged except the exit. The old line:

strategy.exit("Exit Long", "Long", profit=atrVal * 2, loss=atrVal) 

The new lines:

if inTrade entryPrice = strategy.position_avg_price strategy.exit("Exit Long", "Long", limit=entryPrice + atrVal * 2, stop=entryPrice - atrVal) if barsInTrade >= maxHold strategy.close("Long", comment="Time exit") 

limit and stop are price levels, so the bracket is now what post #2 always claimed it was: take profit 2×ATR above the entry price, stop loss 1×ATR below it. One detail worth stating plainly: the bracket is re-placed on every bar while the trade is open, using that bar's ATR, so the levels drift a few points as volatility changes — the stop is not frozen at the entry bar's ATR. That is the natural reading of the code as written, and it is what ran.

The Python is the actual output of the converter, verified tonight with the local compiler (node bin/pine2pyne.mjs, zero diagnostics). The changed lines, verbatim:

 if inTrade: entryPrice: Series[float] = strategy.position_avg_price strategy.exit("Exit Long", "Long", limit=(entryPrice + (atrVal * 2)), stop=(entryPrice - atrVal)) if (barsInTrade >= _pyne_in_maxHold): strategy.close("Long", comment="Time exit") 

Same harness note as post #4, stated plainly: the run copy moves the four input.int calls into main() defaults, because stock PyneCore (GitHub master) rejects the converter's hoisted-input style. Same values, same script.

The first trade: dead code comes alive, 2.76 points from dying

Trade #1 is the same signal in both runs — RSI crosses up through 30 on 2010-02-03 (bar 22), entry the next day at 3206.43. In post #4 it exited one bar later at -$176.13. Here is what happens with a real bracket. ATR on the entry bar is 79.27, so the bracket is limit 3364.96, stop 3127.16:

Bar Date Low High Stop (placed prior bar) Closest call
24 2010-02-05 3129.92 3177.42 3127.16 Low misses stop by 2.76
25 2010-02-08 3133.81 3172.31 3126.48 Misses by 7.33
26 2010-02-09 3144.48 3178.95 3129.44 Misses by 15.04
31 2010-02-23 3152.93 3225.39 3142.68 Misses by 10.25
33 2010-02-25 3249.33 3294.14 3140.44 Time exit fires

The trade nearly dies on the first bar after entry — the low comes within 2.76 points of a stop that is 79 points wide. It survives, drifts upward for nine more bars, and on bar 33 the counter hits 10: the time exit fires at the bar's open, 3252.15, for +$137.16. The same trade, the same signal, the same entry price: -$176.13 in post #4, +$137.16 here. A $313.29 swing on one trade, from one line. The time exit is no longer dead code — it fires on the very first trade of the run.

The aggregate: the fix is real, and the strategy is worse

Six time exits fire across the run, five of them winners, net +$845.87. That is the part the fix was supposed to deliver, and it delivers it. But the bracket exits tell the other half: 52 trades exit on the bracket, only 14 of them winners, net -$3,418.54. Add them together and you get the headline: -$2,572.67, worse than the tick bracket's -$1,353.29.

Why doesn't the fix help? Because the fix changes what the strategy is, not whether it works. Three things the aggregate shows:

1. A real bracket still exits fast. 23 of the 52 bracket exits happen one bar after entry. The bracket is wide — 2×ATR of profit, 1×ATR of stop, against a median daily range of about 46 points — but a mean-reversion entry after an oversold bounce is, by construction, an entry into a falling market. The stop gets hit on big down days before the bounce has room to develop. The widest losing trade is the proof: trade #3 enters 2010-05-06 at 3014.91 with the stop at 2945.17, and the next bar gaps straight through it, opening at 2835.88 — 109 points below the stop — and fills at the open for -$537.09. A stop loss does not protect you from a gap. It never did.

2. The time exit is a small-sample hero. Six fires in 58 trades, five winners. That looks like a good rule until you notice the denominator: the rule only gets a chance to fire when the bracket doesn't fire first for ten bars, which happens 6 times out of 58. Its +$845.87 is real money in this run, but it is six observations. I am not going to call a 6-trade sample a finding.

3. Longer holds desynchronize the trade list. The first 39 entries are identical between the two runs. After that, the lists part ways: a trade that lasts 10 bars in this run is still holding when the next signal fires, so the signal is skipped, while the tick-bracket run — flat after every trade — takes it. 59 of the 60 entry dates from post #4 still appear here, but in a different order and combination. Once two backtests desynchronize by one trade, every later difference is partly a consequence of the first. You cannot fix that by patching the trade list. You fix it by fixing the units — which is what this post did — and then accepting the new list on its own terms.

Both are true at once

The fix is correct: limit/stop is what the strategy meant, the time exit now fires, and trade #1 proves it bar by bar. And the strategy is worse: -25.73% against -13.53%, profit factor 0.587 against 0.695, win rate 33% against 45%. If you only read one of those sentences you walk away with the wrong lesson. The wrong lesson from post #4 would have been "the strategy loses 13.53%". The wrong lesson from this post would be "the fix made it worse, so don't fix your brackets". The honest reading is narrower: the strategy, as intended, loses 25.73% on this data, and the strategy as written in post #2 was never run at all — its bracket was a rounding error. Neither number is the strategy's "true" result in any larger sense. Both are the measured result of a specific reading of the code on a specific 4,089 bars. The only claim this series will defend is that the reading is now the intended one, and the numbers are what that reading produces.

Rules I now follow

  1. Fix the units before judging the strategy. A backtest of the wrong rule is not a pessimistic backtest. It is a backtest of a different strategy. Post #4's -13.53% was never this strategy's number.
  2. Quote the exit count, not just the net. "Time exit fired 6 times" is the check that the fix landed. Net profit alone cannot tell you whether the rule you think you tested is the rule that ran.
  3. A gap through the stop is a fill, not a bug. Trade #3's -$537.09 is the largest loss in the run and it is exactly what a stop order does when the market opens below it. If your backtest never shows a gap-through fill, your fill model is flattering you.
  4. Six observations is not a finding. The time exit's 5-of-6 winners is a reason to keep the rule in the next test, not a reason to claim it works.
  5. Change one line, re-run everything. The whole post is a one-line diff. The whole result changed. That is the only sense in which backtesting is cheap.

Next in the series: the diagnostics teardown — the 55 diagnostics from the 1,909-script corpus, what each family actually means, and the one diagnostic that silently changes your strategy's meaning.

Which fix changed your numbers most — a unit, a fill, or a warm-up? I read every comment — the fix stories are what the next posts are built from.

This conversion work was done by Pine2Py, the tool I build for exactly this job: paste a Pine script, get PyneCore-flavored Python back, diagnostics included when something can't be translated. The runtime is the open-source PyneCore project, with a patched build we ship for the two constructs stock PyneCore doesn't handle — details are on the site. It's free to try — every account gets 3 conversions, no card — and Pro is $12/month if you end up converting a whole library. No return promises, ever: a translator moves your logic, it doesn't make it profitable.