Why your RSI divergence alert fires at the worst possible time.
RSI divergence is one of those signals that looks gorgeous in hindsight. Every backtest you see on Twitter features a perfect lower-low on price with a higher-low on RSI, followed by a textbook bounce. In live trading, it fires at exactly the wrong moment — the instant the price has already reversed. Here is why that happens, and what we changed in our loop to stop it.
The forming-candle problem
Most retail platforms recompute indicators on every tick. It feels responsive, but it means your RSI value for the current 15-minute window is being derived from an incomplete candle. That value will shift — sometimes substantially — as the remaining 11 minutes of order flow arrive. A divergence that appears at minute 4 often disappears by minute 15.
Which means the alert you received at minute 4 was based on a world-state that will no longer exist when your order fills.
A concrete example
15 March 2026, BTC-USDT, 15m timeframe. Price made a lower low at $81,430. The platform's live RSI showed 27.8 against a prior low of 29.1 — a textbook bullish divergence. Alert fires at minute 6 of the candle.
By the time the candle closed, an aggressive seller had pushed the low down another 0.4%, RSI closed at 26.4 — no longer a divergence, just a lower low confirming the downtrend. Users who acted on the minute-6 signal were long into a continuation leg.
The signal was never wrong. The timing was wrong. The indicator was reading from a snapshot that had not happened yet.
Why this is systemic, not a bug
It is systemic because every "live" RSI on every retail charting platform works this way by default. The reason is UX: users want to see the line move. They want to feel the market. Charts that only update on bar close feel dead. So platforms compute against the forming candle and hope the user has the discipline to wait.
Users do not have that discipline. They see the divergence form, they see their alert fire, they enter the trade. The platform did not lie to them — it just showed them a state that had a 40–60% probability of not surviving to bar close.
The fix
We rewrote the indicator loop around a single, boring rule:
Evaluate signals on closed candles only.
In our code this lives in one utility:
# backend/app/engine/utils.py
def strip_forming_candle(candles: list[dict], timeframe: str) -> list[dict]:
"""Drop the in-progress candle. Evaluation is done on closed bars only."""
...
Every adapter — RSI, MSS, OBI — calls this before handing the bar list to compute_mood or compute_events. There is no configuration knob. There is no "fast mode." The forming candle is always stripped.
Why it took three months
Because the hard part was not the strip function. The hard part was rebuilding every consumer of bar data — chart, screener, signals, RSI 2.0, MSS adapter — to share one source of truth. We call it the autonomous-adapter pattern:
- Candle Service closes a bar and publishes
candles:readyto Redis. - Each adapter — exactly one per indicator — listens and runs its cycle.
- Results are written once: mood, events, and (for MSS) a cached structure dict.
- The chart, screener, and signal matcher all read the same keys. No recomputation.
Before: 40+ code paths recomputing RSI, each with their own forming-candle bug. After: one code path, one pub/sub event, one set of keys.
What you should do about it
If you are on another platform:
- Treat every live indicator reading as provisional. It will move.
- Wait for bar close before acting on divergence signals. Yes, you will get fewer trades. Yes, that is the point.
- Prefer indicators that are only published on close (MACD histogram cross, volume-weighted RSI) over those that update every tick.
If you are on TradeFloor, you do not need to think about this. Every signal that fires in your inbox — RSI level, RSI divergence, MSS shift, OBI threshold — has already survived its own bar close. The signal that appears to you is the signal that survived.
The four kinds of liquidation, and how to budget for each →
Margin call, cascade, funding bleed, and the one nobody talks about.
// questions or corrections · [email protected] · more essays · /blog