SHIPPED2026-07-03 · 8분 · 트레이딩

The look-ahead bug that made our backtest look worse

Our Chandelier exit peeked one bar ahead, cutting winners short instead of inflating the result. A fixed backtest is not a found edge: ETH 4h went from -16.2% to +185.0%.

The backtest was cheating, and the cheat was making it lose.

That is the part that took a while to see. Look-ahead bias normally enters the story as a flattering mistake: tomorrow’s information leaks into today’s decision, the equity curve improves, and somebody eventually discovers that the apparent edge cannot exist. Ours did the opposite. A one-bar look-ahead in an ATR-Chandelier trailing stop pulled the stop too close, shook winning trades out early, and made the strategy look substantially worse than it was.

On the committed backtest engine, removing that bug moved ETH 4h for 2025 from -16.2% to +185.0%. Every coin in the four-coin confirmation improved. That was not evidence that we had found a deployable strategy. It was evidence that we had found the wrong clock inside our simulator.

Research write-up, not financial advice.

No live-trading stage was running. This was a research-only exit path. The production research configuration used donchian_breakout with E_triple_barrier, not the Chandelier exit, so the bug did not affect it.

The clue was not the return

The investigation started with a gap between our macd_rsi 4h backtest and the same setup in TradingView Pine. On ETH, our parity harness reported -43.0% while TradingView reported +79.2%.

A giant return gap is not, by itself, a useful clue. Different data, fills, fees, windows, entries, and exit rules can all produce one. The useful clue was that the systems were still taking almost the same trades: 110 in our run and 108 in TradingView, with similar win rates. We were not finding different entries. We were getting less out of the same general set of positions.

The gap was also systematic across the comparison windows. That narrowed the search. If the entries mostly agree and the winners are consistently smaller, inspect the exit timing before rewriting the signal.

Our first suspicion was TradingView. Optimistic fills or Pine behavior would have been a convenient explanation. It was also wrong.

One switch explained the gap

We forked the parity harness into an ablation script and changed one execution assumption at a time. The baseline had to reproduce the existing result exactly before any variation counted.

Changing the entry fill to the next open did not close the gap. Adding gap-through fills moved the result the wrong way. Only one switch did the work: stop_timing=lagged.

ETH 4h, 2025 Return
Current stop timing -43.0%
Prior-bar stop timing +87.6%
TradingView +79.2%

The cross-coin result mattered more than the ETH result. Across the 10 TradingView coins, Pearson correlation with TradingView rose from +0.51 to +0.91. Spearman rank correlation rose from +0.31 to +0.89. Trade counts barely changed. The same entries were staying open longer.

That localized the problem to one bar of stop timing. It did not yet tell us which implementation was faithful.

The impossible stop

A Chandelier exit trails a long position below its highest high by a multiple of ATR. The implementation looked natural when read from top to bottom:

# The buggy order on bar b
extreme = max(extreme, high[b])
stop = extreme - mult * atr[b]
if low[b] <= stop:
    exit_position()

The bug hides inside the word “bar.” A 4h OHLC bar gives the final high and low for the same four-hour interval, but it does not tell you which happened first. The code used the completed bar’s high to tighten the stop, then asked whether the same bar’s low had crossed that newly tightened level. In real time, that stop did not exist yet.

For a realizable simulation, the stop active during bar b must be the one fixed at the close of bar b-1. Only after bar b closes can its high ratchet the stop for the following bar:

# The realizable order on bar b
stop = extreme_through_previous_bar - mult * atr[b - 1]
if low[b] <= stop:
    exit_position()
else:
    extreme_through_previous_bar = max(extreme_through_previous_bar, high[b])

Why did seeing the future hurt? Because the leaked high did not help the entry. It tightened a trailing stop. A bar could print a new high, pull back, and then be declared stopped out against a level that only became knowable after the bar completed. The look-ahead systematically truncated winners.

Pine was the faithful implementation

We audited the Pine strategy against our signal and execution assumptions. The entry rule matched. The strategy settings matched the comparison model. There was no repaint source hiding in a cross-timeframe request.

The decisive detail was strategy.exit. Pine calculates and places the Chandelier stop at the close of one bar. That order becomes active for the next bar. During bar b, TradingView is therefore checking the stop built from the extreme and ATR available through bar b-1.

Our engine did the reverse: it incorporated bar b into the stop and then tested bar b against it. TradingView was not inflating the backtest. It was respecting the sequence in which the information became available.

This was a useful correction in more than code. “The external platform must be optimistic” was the emotionally easy answer. The ablation made us inspect our own ordering before accepting it.

The tests had to catch the timing, not the return

A regression test based on a portfolio return would have been broad, slow, and easy to disturb with unrelated model changes. The invariant was smaller: a bar that makes a new high must not use that high to stop itself out.

The first test constructs exactly that case. A long position enters with an extreme of 100 and an ATR of 1. The next bar trades up to 110 and down to 106. With a multiplier of 3, the realizable stop is still 97, built from the prior extreme. The buggy stop is 107, built from the current high, so the same low incorrectly triggers it.

# Prior-bar stop: 100 - 3 * 1 = 97, so low 106 survives.
# Look-ahead stop: 110 - 3 * 1 = 107, so low 106 exits early.
assert simulator.exit_reasons["chandelier_stop"] == 0
assert simulator.positions["BTC-USD"].chandelier_extreme == 110.0

The companion test proves the fix did not merely disable the stop. On the following bar, the stop built from the new 110 extreme is active at 107, and a low of 106 closes the position there.

Both tests went RED against the old ordering and GREEN after the fix. The full Chandelier surface then passed 126/126 tests, with lint clean.

The engine change was deliberately boring: read the previous bar’s ATR, check the stored prior-bar extreme against the current bar, and ratchet the extreme only after the check.

The corrected result, and the correction it forced

We then ran the real engine before and after the change on identical macd_rsi 4h configurations:

Coin Before fix After fix
ETH -16.2% +185.0%
BTC -33.6% -12.9%
ADA -25.0% +17.9%
LINK -25.7% -18.9%

Every coin improved. The exit mix shifted away from early Chandelier stops and toward more timeouts, which is the mechanical fingerprint we expected when winners were allowed to ride.

The +185.0% ETH result is not an apples-to-apples TradingView comparison. The committed engine does not flip on opposite signals, while the TradingView model does. The matched flip-model comparison is the ablation result: +87.6% in our corrected harness against +79.2% in TradingView. Exit model and look-ahead timing are separate axes, and collapsing them would turn a real fix into a false claim.

The bug also forced us to correct an earlier verdict. We had reported that the Chandelier exit was worse than the triple-barrier baseline across the comparison. The look-ahead penalized only the Chandelier path, so that comparison was confounded. The numbers were real, but the interpretation was not.

A fixed backtest is not a found edge

It would be easy to end on the ETH number. It would also be the wrong ending.

After the correction, the strategy still behaved like trend exposure: ETH and ADA rose, BTC and LINK remained negative, positive breadth was about 50%, and drawdowns remained 40-64%. The underlying macd_rsi entry had approximately zero information coefficient in the broader research. A faithful trailing stop can let a trend run. It cannot manufacture an edge in the entry.

So the deployability verdict still stands. What changed was the reason. The strategy was not dead because the Chandelier exit was uniformly worse. It remained undeployable because the corrected result was trend-beta on an edgeless entry.

That distinction is the point of maintaining a research log instead of a victory reel. We fixed the engine, amended the comparison, kept the strategy out, and added two tests that make the same temporal mistake harder to repeat.


Research only. No live-trading stage was running or affected. The percentages above are historical backtest results from our own session log, not forecasts or promises.

The backtest was cheating, and the cheat was making it lose.

LAB NOTES · 2026-07-03
실행 후 회고
배운 것
  • +A look-ahead bug can make a backtest too pessimistic when it tightens a trailing stop before the bar is complete
  • +Ablate one execution assumption at a time before blaming the reference platform
  • +The active stop for a bar must be built only from information available before that bar
  • +Fixing a backtest can correct the result without turning the strategy into something deployable
망가진 것
  • ×The Chandelier exit ratcheted its extreme with the current bar before testing that same bar for a stop
  • ×The current bar's ATR also leaked into a stop that was supposed to be active during that bar
  • ×The biased comparison made the Chandelier exit look systematically worse than the triple-barrier baseline
  • ×Our first suspicion pointed at TradingView even though its order timing was the faithful one
FALSIFIED

Our LLM Polymarket bot was just reading the market price back to us

Aggregate Brier score 0.12. And it lost to the market in every single edge bucket. A postmortem of an LLM forecaster that turned out to be an expensive mirror.

2026-06-07 · 8분트레이딩
SHIPPED

GPU passthrough into LXC, without the pain

Four identical RTX 5060 Ti cards, one Proxmox container, and a PCIe lane map that quietly decides which card is allowed to talk to which. The passthrough was the easy part.

2026-08-11 · 12분홈랩
THE BRIEF

무엇이 돌아갔고, 무엇이 출시됐고, 무엇이 죽었는지 — 숫자와 함께. 스레드도, 과장도 없습니다.

언제든 구독 해지 · RSS 제공