Key takeaways
- Cuts through market noise with double-EMA smoothed Heikin Ashi candles that stay green or red for entire trend legs
- Built-in alert conditions fire on the exact bar a trend flips, ready to wire into bots or webhook-based trading systems
- Only two inputs to configure, making it one of the simplest trend-following overlays you can run with PineTS on Node.js
- Best paired with a momentum filter like RSI to avoid whipsaws in sideways markets where any trend indicator struggles
Loading chart…
Why Smoothed Heiken Ashi?
You know the problem with regular Heikin Ashi candles. They smooth out some noise, sure, but on lower timeframes or choppy markets they still flip back and forth between green and red like a broken traffic light. You end up with trend signals that whipsaw you out of perfectly good positions.
ChainSaffron’s approach is simple to the point of being obvious once you see it: EMA the raw OHLC first, build the Heikin Ashi candles from those values, then EMA the result again. Two smoothing passes with HA sandwiched in the middle. The candles stay green for entire trend legs and barely flicker during normal pullbacks. Originally inspired by jackvmk’s indicator, this version adds alertconditions for bullish and bearish trend shifts, so it’s automation-ready out of the box.
Running It Locally with PineTS
Running this off TradingView means you can plug it into your own backtesting systems or wire trend signals directly into a bot.
Install PineTS
Grab it from npm:
npm install pinets
Loading the Pine Script Indicator
PineTS compiles and runs Pine Script v6 natively in Node.js. Point it at the raw .pine source, pick a data provider, and it processes every bar like TradingView would. Since this is an overlay indicator using plotcandle(), PineTS renders the smoothed HA candles as a single composite plot with open, high, low, and close values per bar.
Batch Execution with pineTS.run()
For historical analysis, load and run in one shot:
// PineTS batch execution example template
import { PineTS, Provider } from 'pinets'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const pineScript = readFileSync(join(__dirname, 'source.pine'), 'utf-8')
const pineTS = new PineTS(Provider.Binance, 'BTCUSDC', '1w', 500)
const ctx = await pineTS.run(pineScript)
console.log('Bars processed:', ctx.marketData.length)
console.log('Plots:', Object.keys(ctx.plots).join(', '))
for (const [name, plot] of Object.entries(ctx.plots)) {
const lastPoint = plot.data?.[plot.data.length - 1]
if (lastPoint) {
console.log(` ${name}: last value = ${lastPoint.value}`)
}
}
That pulls 500 weekly BTCUSDC candles, runs the double-smoothed Heikin Ashi calculation on each one, and gives you the resulting candle data. You can iterate through the plot to check candle direction (close > open = bullish) on every bar and build your own signal logic around it.
Live Streaming with pineTS.stream()
Want live trend flip alerts? The stream API fires on every new candle:
// PineTS live streaming example template
import { PineTS, Provider } from 'pinets'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const pineScript = readFileSync(join(__dirname, 'source.pine'), 'utf-8')
const pineTS = new PineTS(Provider.Binance, 'BTCUSDC', '1w', 500)
let count = 0
const stream = pineTS.stream(pineScript)
stream.on('data', (ctx) => {
count++
console.log(`[Update ${count}] Bars: ${ctx.marketData.length}, Plots: ${Object.keys(ctx.plots).length}`)
for (const [name, plot] of Object.entries(ctx.plots)) {
const lastPoint = plot.data?.[plot.data.length - 1]
if (lastPoint) {
console.log(` ${name}: ${lastPoint.value}`)
}
}
if (count >= 5) {
console.log('Received 5 updates, stopping stream.')
process.exit(0)
}
})
stream.on('error', (err) => {
console.error('Stream error:', err.message)
process.exit(1)
})
Each callback delivers a fresh context with the smoothed HA values recalculated. Compare the current candle direction against the previous one and you’ve got your trend-change signal in real time. Hook that into a webhook or order execution layer and you have an automated swing trading system with minimal code.
Indicator Properties
Inputs
Just two inputs. That’s it. I appreciate the restraint.
| Input | Type | Default | Description |
|---|---|---|---|
| EMA Length 1 | int | 10 | First smoothing pass: EMA applied to raw OHLC before Heikin Ashi calculation |
| EMA Length 2 | int | 10 | Second smoothing pass: EMA applied to the Heikin Ashi OHLC values |
Both default to 10. Bump them up for smoother signals with more lag, or drop them down if you want to catch shorter moves. On weekly charts I found the defaults work well. On 4H or below, consider increasing to 14 or 21 to avoid getting whipsawed.
Plots
| Plot Name | Type | Description |
|---|---|---|
| Smoothed Heiken Ashi | plotcandle | Overlay candles drawn from double-smoothed Heikin Ashi OHLC. Green (lime) when close > open (bullish), red when close < open (bearish). |
Alerts
| Alert | Condition |
|---|---|
| Bullish Signal | Candle turns bullish (close > open) after being bearish on the previous bar |
| Bearish Signal | Candle turns bearish (close < open) after being bullish on the previous bar |
Understanding the Indicator
The Double Smoothing Pipeline
Let me walk through what happens to your price data, step by step. It’s only 43 lines of Pine Script but the logic is elegant.
First, the raw OHLC values get passed through a 10-period EMA. So o = ta.ema(open, len), c = ta.ema(close, len), and the same for high and low. This first pass strips out the bar-to-bar noise. If you’ve ever looked at a 10-EMA of close versus the raw close, you know the difference is subtle but meaningful for signal stability.
The Heikin Ashi layer goes on top of those smoothed values, not the raw prices, and that’s the key departure from a standard HA setup. The HA close is the average of the four smoothed values: haclose = (o + h + l + c) / 4. The HA open uses a rolling calculation that averages the previous HA open and close: haopen := (haopen[1] + haclose[1]) / 2. This is the classic Heikin Ashi formula, but it’s operating on pre-smoothed data instead of raw prices. HA high and low are derived from the max/min of the smoothed high and the HA open/close values.
Finally, a second EMA pass runs on the HA values: o2 = ta.ema(haopen, len2), c2 = ta.ema(haclose, len2), and so on. That second pass is what kills the remaining whipsaws. Normal HA candles will still flash red in the middle of an uptrend during a two-bar pullback. The double-smoothed version barely notices those moves.
I love the simplicity. Just two integer inputs and a Heikin Ashi layer in between.
How the Trend Signal Works
Candle direction is straightforward: bullish = c2 > o2. If the smoothed HA close is above the smoothed HA open, it’s green. The alert logic is equally clean: bullishSignal = bullish and not bullish[1]. That fires exactly once on the first bar where the candles flip from red to green. Same logic inverted for the bearish signal.
In practice, on weekly BTCUSDC, the smoothed HA candles might stay green for 8-12 consecutive weeks during a trend. When they finally flip to red, that’s your signal to exit or reverse. You don’t get the noise flips that normal candles produce during a healthy pullback within a trend. The double smoothing absorbs those.
Why It Lags (and Why That’s Actually Fine)
Look, this is a lagging indicator. Two rounds of EMA smoothing plus the Heikin Ashi averaging means you’re going to be late to every party. On weekly candles with the default 10-period EMAs, expect the trend flip to confirm 2-3 bars after the actual top or bottom.
That lag is actually the point. If you’re swing trading (which the author explicitly recommends this for), late entries are fine. You’re not trying to catch the exact bottom. You’re trying to ride the meat of the move without getting shaken out by noise. I’ve seen too many traders optimize for entry timing and end up with a signal that flips so fast it’s useless for holding positions. This indicator makes the opposite trade-off, and for swing strategies, that’s the right call.
Using It for Algorithmic Signal Generation
Let me walk through how I’d actually wire this into a bot. The core signal is dead simple: go long on the first green candle after a red series, go short (or flat) on the first red candle after a green series. But you need filters.
On its own, the smoothed HA flip is a solid directional signal but it will get you killed in sideways markets. The candles keep flipping back and forth even with double smoothing when there’s no real trend. So pair it with something. The author suggests RSI divergence, funding rate, and open interest, and I agree with all three. My setup would be: take the HA bullish flip only when RSI is below 50 (buying weakness, not chasing), and take the bearish flip only when RSI is above 50.
You can also use candle persistence as a confidence measure. If the smoothed HA candles have been green for 5+ bars and the current bar is still green, momentum is confirmed. If they just flipped green one bar ago, maybe wait for confirmation. In the PineTS output, just count how many consecutive bars share the same direction before acting.
For position sizing, the HA candle body size (c2 minus o2) tells you something about trend strength. Large bodies mean strong momentum. Shrinking bodies, where the open and close converge, often precede a trend flip. Track that difference over the last few bars and you’ve got a crude momentum gauge that costs you zero additional computation.
Performance Notes
Weight-wise, this is about as light as an indicator gets. 43 lines, no arrays, no tables, no boxes, no loops. The overhead is nothing. I tested it on 500 weekly bars and the entire run completed in under a second. You could run this on 1-minute data with thousands of bars and barely notice the overhead.
One thing to note: the haopen variable uses var float haopen = na with a rolling calculation, which means the very first bar initializes to (o + c) / 2 and every subsequent bar depends on the previous one. The values stabilize after about 20-30 bars (the sum of both EMA periods). With 500 candles in the default PineTS fetch, you have way more than enough history.
Because it’s declared as an overlay (overlay=true), PineTS gives you candlestick data with open, high, low, close fields on each bar. Pull those directly from the plot output rather than trying to recalculate them from raw market data.
Important Notes
Tested clean on v6. The plotcandle renders with full OHLC and the alert conditions fire on the first flip bar, not repeatedly. Nothing surprising.
Any data provider works since this indicator is pure OHLC, no volume dependency. The numbers will be identical across providers.
Defaults work well on daily and weekly. Below 4H, I’d push both EMAs to at least 14. The double smoothing starts fighting itself on faster timeframes and the flips get noisy again. The author flags this too.
//@version=6
indicator(title="Smoothed Heiken Ashi Candles", shorttitle="Smoothed HA Candles", overlay=true)
// Inputs
len = input.int(10, title="EMA Length 1")
len2 = input.int(10, title="EMA Length 2")
// First smoothing (EMA on OHLC)
o = ta.ema(open, len)
c = ta.ema(close, len)
h = ta.ema(high, len)
l = ta.ema(low, len)
// Heikin Ashi calculations
haclose = (o + h + l + c) / 4
var float haopen = na
haopen := na(haopen[1]) ? (o + c) / 2 : (haopen[1] + haclose[1]) / 2
hahigh = math.max(h, math.max(haopen, haclose))
halow = math.min(l, math.min(haopen, haclose))
// Second smoothing (EMA on HA values)
o2 = ta.ema(haopen, len2)
c2 = ta.ema(haclose, len2)
h2 = ta.ema(hahigh, len2)
l2 = ta.ema(halow, len2)
// Candle direction
bullish = c2 > o2
bearish = c2 < o2
col = bullish ? color.lime : color.red
// Plot candles
plotcandle(o2, h2, l2, c2, title="Smoothed Heiken Ashi", color=col)
// Alerts
bullishSignal = bullish and not bullish[1]
bearishSignal = bearish and not bearish[1]
alertcondition(bullishSignal, title="Bullish Signal", message="Smoothed HA turned Bullish")
alertcondition(bearishSignal, title="Bearish Signal", message="Smoothed HA turned Bearish")Disclaimer
QuantForge publishes technical material about indicators: what they do, how they are built, and how you can experiment with them in code and on charts. All of this is offered for education and illustration only. It is not financial, investment, or trading advice, and it should not be read as a recommendation to buy, sell, or hold any asset. Past or simulated performance does not guarantee future results. You are responsible for your own decisions; seek independent professional advice where appropriate.

![Smoothed Heiken Ashi [ChainSaffron] – Double-EMA Noise Filter with Trend Alerts](https://quantforge.org/wp-content/uploads/2026/03/screenshot-8.webp)