Backtesting Heikin Ashi Candle Colors to Buy Dips and Exit When Runs End
Daily OHLC candles reflect in an unfiltered way the components of a daily trading day based on the open, high, low, and close prices during a trading day. Traders benefit from this data because today’s close price less yesterday’s close price can confirm if and by how much prices rose or fell today versus yesterday. Similarly, the open price from today less close price from yesterday can confirm if and by how much a security’s prices changed at the market open today relative to yesterday’s close price.
While OHLC candles facilitate compiling a log of daily price movements, they can also reflect significant visual noise and false momentum signals from routine daily price action within and across trading days. Heikin Ashi candles offer a powerful alternative by applying a set of averaging formulas across adjacent sessions to smooth day-to-day volatility and reduce the effect of random price fluctuations. A trading model based on Heikin Ashi candles leverages these benefits to distinguish a true directional shift from a transient counter-trend move and establish objective, repeatable rules for entering on sustained dips and exiting when momentum runs genuinely exhaust.
This post’s main objective is to introduce and demonstrate the operation of a trading model based on Heikin Ashi candles. The post tracks trade outcomes for a set of eight tickers and a benchmark ticker. The eight non-benchmark tickers reflect trading instruments that are likely to attract traders interested in exploiting relatively short-run trends, such as those detected by Heikin Ashi candles. The post also includes a brief introductory section on Heikin Ashi candles because this is the blog’s initial post on Heikin Ashi candles.
The Nine Tickers Tracked in This Post
This section lists and describes the nine tickers tracked in this post (AAPL, AMD, DELL, GOOGL, MSFT, MU, NVDA, VLO, and SPY). SPY serves as a broad-market benchmark that does not have the same volatility over the evaluation period as the remaining eight tickers. With the exception of SPY, these tickers appeal to active traders basing trade entry and exit dates on Heikin Ashi candle colors. SPY targets investors who seek safety in broad diversification but also prefer capital appreciation from securities that can be traded.
The following image is an excerpt from Finviz that provides a topline summary of the tickers tracked in this post. For example, the tickers are from the Technology, Communication Services, Energy, and Financial sectors, with the Technology sector being overrepresented. The tickers are more diverse at the industry level. All the tickers are for US companies.
This post focuses primarily on trade percentage returns when
trade entry and exit dates are selected based on Heikin Ashi candle
colors. The evaluation period is from
the first trading day in 2017 through September 30, 2026. The historical price data for this post is
derived from GOOGLEFINANCE functions in Google Sheets.
Before presenting highlights of how the GOOGLEFINANCE
function in Google Sheets is used in this post, I want to inform readers that you
can browse the data and supporting GOOGLEFINANCE expression for downloading
historical prices from this url (https://docs.google.com/spreadsheets/d/1nBxDJD4spgv3h6cvbPVcJtjI4GV9HmMwI_9hlHrsEK4/edit?usp=sharing). There is no paywall, no need to have a Google
browser, or even a Google account. The only
feature that requires a free Google account is making an automated
full-workbook copy straight into Google Drive.
Here is an excerpt from the DELL tab in the
First_HA_Ticker_Set workbook that you can access from the preceding url
reference. The cursor rests in cell A1,
and a truncated view of the GOOGLEFINANCE expression appears in the formula
bar. The worksheet shows the initial four
rows of historical dates and prices. You
can verify that the first trading day in January, 2017 is the third day of the
month.
The next workbook excerpt shows the bottom five rows of historical dates and prices in the DELL tab. The cursor rests in the final row of the worksheet with a Date value of 9/30/2026. If you examine the preceding workbook excerpt, you can see that the fourth parameter for the GOOGLEFINANCE expression contains this date. By selecting any of the other eight tabs, you can change the data being displayed in the workbook.
While many US tickers like DELL or SPY pull price data cleanly in the GOOGLEFINANCE function without an exchange code, certain equities or international stocks require explicit prefixes (e.g., NASDAQ:AAPL or TSE:SHOP). If you are unsure whether a specific ticker requires an exchange prefix, you can ask an AI assistant (like Google Gemini or Microsoft Copilot): "What is the correct Google Finance exchange prefix for ticker [XYZ]?"
After you extract historical prices with the GOOGLEFINANCE function for whatever tickers you are tracking, you can download historical prices from each tab to your default Downloads directory with the Google Sheets File → Download → Comma Separated Values (.csv) command. Then, you can migrate the csv files to whatever target directory you are using to store the historical prices for your ticker set. This post uses the “C:\Users\User\Documents\First_HA_Ticker_Set\” folder for storing csv files with historical data for each ticker.
A SQL Server Dynamic Ingestion Script for Downloaded CSV Files
Ingestion of csv files into a SQL Server table can be a routine task. Here is a T-SQL script that dynamically ingests the csv files for the nine tickers tracked in this post from the "C:\Users\User\Documents\First_HA_Ticker_Set" folder.
As you can see, the first four steps transfer csv files from the source directory of csv files from GOOGLEFINANCE worksheets for each of the tickers tracked in this post.
- Step 1 creates a staging table for the csv files that correspond to each ticker.
- Step 2 creates a clean table for storing the data for all nine tickers.
- Step 3 configures a folder’s path for the source csv files as well as a temporary table of tickers. When you are adapting this process to other tickers in different folder paths, you will need to update @folder_path local variable and the #tickers temp table contents.
- Step 4 uses the file_cursor cursor and a dynamic sql string (@sql) to populate the dbo.stockhistory_clean_gs table with the csv file contents.
- Step 5 returns a result set that compares all nine tickers in terms of row count, min_date, and max_date.
/* dynamic ingestion for googlefinance csv files into a
tall sql table ver_4.4
date: 10/3/2026
authors: rick
dobson & Google Gemini
purpose: load
5-column googlefinance csv exports with full OHLC price data,
convert
date/numeric fields directly (decimal 18,6),
populate dbo.stockhistory_clean_gs,
and
verify row counts as well as first and last dates per ticker.
*/
use securitytradinganalytics;
go
---------------------------------------------------------
-- 1. recreate staging table (5 columns matching CSV
layout)
---------------------------------------------------------
if object_id('dbo.stockhistory_stage', 'U') is not null
drop table
dbo.stockhistory_stage;
create table dbo.stockhistory_stage
(
[date] varchar(50),
[open] varchar(50),
[high] varchar(50),
[low] varchar(50),
[close] varchar(50)
);
---------------------------------------------------------
-- 2. recreate clean table for google finance (_gs)
---------------------------------------------------------
if object_id('dbo.stockhistory_clean_gs', 'U') is not
null
drop table
dbo.stockhistory_clean_gs;
create table dbo.stockhistory_clean_gs
(
[date] date
not null,
[open] decimal(18,6) not null,
[high] decimal(18,6) not null,
[low] decimal(18,6) not null,
[close] decimal(18,6) not null,
[ticker]
varchar(20) not null,
constraint
pk_stockhistory_clean_gs primary key clustered ([ticker], [date])
);
go
---------------------------------------------------------
-- 3. configure folder path and tickers
---------------------------------------------------------
declare @folder_path varchar(255);
set @folder_path =
'C:\Users\User\Documents\First_HA_Ticker_Set\';
if object_id('tempdb..#tickers') is not null
drop table
#tickers;
create table #tickers
(
ticker
varchar(20) not null
);
insert into #tickers (ticker)
values
('AAPL'),
('AMD'), ('DELL'), ('GOOGL'), ('MSFT'),
('MU'),
('NVDA'), ('VLO'), ('SPY');
---------------------------------------------------------
-- 4. iterate through cursor and process files
---------------------------------------------------------
declare @ticker varchar(20);
declare @file_path varchar(500);
declare @sql nvarchar(max);
if cursor_status('global', 'file_cursor') >= -1
begin
close
file_cursor;
deallocate
file_cursor;
end;
declare file_cursor cursor for
select ticker from #tickers;
open file_cursor;
fetch next from file_cursor into @ticker;
while @@fetch_status = 0
begin
truncate table
dbo.stockhistory_stage;
set @file_path
= @folder_path + @ticker + '.csv';
set @sql = '
bulk insert
dbo.stockhistory_stage
from ' +
nchar(39) + @file_path + nchar(39) + '
with
(
firstrow =
2,
fieldterminator = '','',
rowterminator = ''\n''
);';
exec
sp_executesql @sql;
---------------------------------------------------------
-- direct
insertion of clean rows with injected @ticker
---------------------------------------------------------
insert into
dbo.stockhistory_clean_gs ([date], [open], [high], [low], [close], [ticker])
select
try_convert(date,
s.[date]) as [date],
try_convert(decimal(18,6), s.[open]) as [open],
try_convert(decimal(18,6), s.[high]) as [high],
try_convert(decimal(18,6), s.[low]) as [low],
try_convert(decimal(18,6), s.[close]) as [close],
@ticker as
[ticker]
from
dbo.stockhistory_stage s
where
try_convert(date, s.[date]) is not null
and
try_convert(decimal(18,6), s.[open]) is not null
and
try_convert(decimal(18,6), s.[high]) is not null
and
try_convert(decimal(18,6), s.[low]) is not null
and
try_convert(decimal(18,6), s.[close]) is not null;
fetch next from
file_cursor into @ticker;
end;
close file_cursor;
deallocate file_cursor;
go
---------------------------------------------------------
-- 5. verification check
---------------------------------------------------------
select
ticker,
count(*) as
row_count,
min([date]) as
min_date,
max([date]) as
max_date
from dbo.stockhistory_clean_gs
group by ticker
order by ticker;
Here is the result set from the preceding script. During DELL's re-listing transition in late
December 2018, GOOGLEFINANCE omitted a single trading day (12/28/2018). As a
consequence, DELL's dataset contains 2,448 rows compared to 2,449 rows for the
other eight tickers.
Here is a relative price performance chart from Finviz for the full set of nine tickers tracked in this post, spanning Jan 3, 2017 through September 30, 2026. Finviz provides an excellent external cross-check confirming that all nine securities—including DELL—share an identical multi-year evaluation window. As the chart shows, DELL achieved a massive 3,415.60% return across the period, with a substantial portion of that surge occurring between the second half of 2025 and September 30, 2026.
This strong multi-year trend is precisely the type of price movement that attracts active traders using Heikin Ashi candles to isolate momentum and filter out noise. When evaluating trading models, it is essential to include strongly trending assets to evaluate how effectively model entry and exit rules for trades capture extended price expansions.
A Quick Tutorial for Heikin Ashi Candles
From an analytical perspective, Heikin Ashi candles differ from standard OHLC candles in how their core values are calculated across adjacent sessions.
- OHLC candles are derived from open, high, low, and close prices for the current trading period, which is often a single trading day.
- Heikin Ashi candle values, except for the first period in a time series, are based on two periods: the current period and the preceding period. The Heikin Ashi close is the average of the current period's open, high, low, and close prices. The Heikin Ashi open is the average of the preceding period's Heikin Ashi open and Heikin Ashi close. Because the preceding Heikin Ashi open was itself computed from the period before it, the Heikin Ashi open acts as a running average of prices through the preceding period.
- The initial period in a Heikin Ashi candle series does not have a preceding period. Therefore, this period requires special expressions to compute its Heikin Ashi candle values.
- The specific expressions used in this post to compute the full set of Heikin Ashi candles appear in the “A Backtest for Entry/Exit Values Based on Heikin Ashi Candle Colors” section.
- In the standard OHLC chart, green candles represent trading days when the close price is above the open price, and red candles represent trading days when the close price is not above the open price.
- In the Heikin Ashi chart, green candles represent trading days when the Heikin Ashi close is above the Heikin Ashi open, that is, when the day's average price is above the running average price through the prior day. Red candles represent trading days where ha_close falls below ha_open.
- The key point to observe is that the runs of green candles in the top chart of Heikin Ashi candles are longer than in the bottom chart of standard OHLC candles.
- This post evaluates the performance of two relatively simple rules for entering and exiting trades based on Heikin Ashi candle colors for each of the tickers tracked in this post.
The code for this section demonstrates how to compute and verify Heikin Ashi candle values and their associated colors in five basic steps. An optional sixth step displays selected column values from the first and last three rows for each ticker that verify the determination of candle colors. If you are going to use Heikin Ashi candles as described in this post, it is imperative that you understand how to assign Heikin Ashi candle colors.
The specific Heikin Ashi values computed in this section are named after their corresponding source candle values – namely, daily open, high, low, and close values. The corresponding Heikin Ashi candle values are named ha_open, ha_high, ha_low, and ha_close. The script populates the dbo.First_HA_Dataset table with ticker and date column values, and open, high, low, and close values from dbo.stockhistory_clean_gs, and calculated ha_open, ha_high, ha_low, ha_close, and ha_color (from ha_open and ha_close) values. The #with_dates temporary table facilitates the implementation of script mechanics.
Here is an overview of the five steps in this script.
- Step1 removes the dbo.First_HA_Dataset table and the #with_dates temporary table so that each run of the script can freshly populate these two tables.
- Steps 2 and 3 work together to populate the dbo.First_HA_Dataset table.
- Step 4 adds a natural key based on ticker and date to the dbo.First_HA_Dataset table.
- Steps 5 and 6 are two verification checks.
- Step 5 is an aggregate verification result set by ticker in which
- source_row_count should match ha_row_count
- flat_bars per is the count of rows where ha_close equals ha_open
- the sum of green_bars and red_bars should equal ha_row_count if ha_open and ha_close never equal each other for unique ticker-date pairs
- Step 6 displays the first and last three rows for each ticker-date
- ha_color should be red whenever ha_open is greater than ha_close
- ha_color should be green whenever ha_open is less than ha_close
Here is the result set from step 5.
Here is the result set from step 6 copied to and formatted in an Excel worksheet. If you have difficulty viewing the values in the display below, consider zooming your view.
A Backtest for Entry/Exit Values Based on Heikin Ashi Candle Colors
This section looks at sequences of ha colors based on ha
opens and closes across consecutive trading days to identify trade entry and
exit dates and prices. The ha values are
calculated from the first trading day in January 2017, and trades are taken
from February 1, 2017 through the last trading day in September 2026, so the
seed used for the first ha open has faded before any trade.
The trading strategy over this timeframe depends on entry
and exit signals.
- The entry signal is defined by a dip over at least three consecutive trading days with red ha color values followed by a single-day recovery (with a green ha color).
- The exit signal is defined by a run of two consecutive trading days with red ha color values. The two-day period can start on or after entry date and before the next entry signal.
- Trade entry dates and prices are, respectively, the first trading date after the entry signal and its open price.
- Trade exit dates and prices are, respectively, the first trading date after the exit signal and its open price.
This strategy is applied separately to eight actively traded stocks (AAPL, AMD, DELL, GOOGL, MSFT, MU, NVDA and VLO) and to SPY as a benchmark, with results measured as percent change per trade.
The following script implements the preceding modeling description and returns two result sets.
- The first result set contains a row for individual trades specified by the model in the dbo.First_HA_Trades table. There is a separate row in this table for each trade designated by model. Column names in the table include ticker, entry_date, entry_price, exit_date, exit_price, trade_pct for percentage return from a trade, and cum_pct for cumulative percentage return for a ticker through the close of that trade. The cum_pct column values reset at the beginning of each ticker to trade_pct value for first trade within a ticker via a sum function’s partition by ticker setting.
- The second result set contains a row for each ticker. The rows are listed in alphabetical order for the eight tickers that traders typically buy and sell on an active basis as well as for the SPY benchmark ticker that investors sometimes buy and hold. By applying the same model to all nine tickers, the benchmark ticker can be compared to the remaining eight tickers and the remaining eight tickers can be compared among one another on an apples-to-apples basis. The results in the second table reflect the outcome from the 3 reds + 1 green in, 2 reds out trading strategy across all nine tickers in terms of their trades count and their final cumulative percentage value.
The code inside the following script consists of five
layers. The first four layers are part
of a layered CTE.
- Layer 1 populates the bars CTE with a separate row for each row in the dbo.First_HA_Dataset. The bars CTE has columns that facilitate the tracking of entry and exit signals along with their associated trade entry and exit dates and prices.
- The first three columns identify the ticker, date, and ha_color of the current row.
- The next three columns store the ha_color values for each of the three prior rows.
- The next column stores the date of the previous row.
- The last two columns store the date and the open price for the next row.
- Layer 2 filters the bars CTE to extract just rows that match the entry signal criteria and then populates the entry_signals CTE entry date and entry price values from the bars CTE.
- Layer 3 filters the bars CTE to extract each row whose ha_color is red and whose prior row’s ha_color is also red. Each extracted row represents one pair of consecutive red days, with the dates of the first and second red days stored in the first_red_date and exit_signal_date columns. Additionally, the associated exit date and exit price values are extracted from the bars CTE.
- Layer 4 populates the trades CTE with the entry date and price as well as the exit date and price for each trade within each ticker. Each entry date is matched with the first exit signal for the same ticker whose two reds both fall on or after the entry date. This layer also enforces the rule of accepting trades that start on or after February 1, 2017. By default, if an entry date does not have a matching exit date that is on or before September 30, 2026, then the trade has null values for exit date and price.
- The final layer updates the rowset from trades CTE by adding trade_pct and cum_pct columns and materializes the outcome as the dbo.First_HA_Trades table.
- Before running the select statements for the two result sets from the following script, a primary key constraint on ticker and entry_date is added to the dbo.First_HA_Trades table.
/* ha color entry/exit trades: 3 reds + 1 green in, 2
reds out ver_1_2
date: 10/10/2026
authors: rick
dobson & Claude
purpose: list
trades for every ticker in dbo.First_HA_Dataset based on
ha
candle colors, save them in dbo.First_HA_Trades, and
summarize the closed trades for each ticker.
rules: entry signal: a green ha bar immediately
preceded by
three consecutive red ha bars
entry: open price on the
trading day after the entry signal
exit
signal: the second of two consecutive
red ha bars,
both on or after the entry date
exit: open price on the
trading day after the exit signal
scope: trades entered on or
after 2/1/2017 (@start_date),
a few weeks after the data starts, so the ha open
seed has faded before any trade
*/
use securitytradinganalytics;
go
-- price data starts 2017-01-03; starting trades a few
weeks later lets the
-- ha open seed (first bar's (open + close) / 2) fade
before any trade
declare @start_date date = '2017-02-01';
drop table if exists dbo.First_HA_Trades;
with
-- layer 1: each bar with the colors of the prior bars,
-- plus
the prior date and the next trading day's date and open,
-- all
computed within each ticker
bars as
(
select
ticker,
[date],
ha_color,
lag(ha_color, 1) over (partition by ticker order by [date]) as
color_1_back,
lag(ha_color, 2) over (partition by ticker order by [date]) as
color_2_back,
lag(ha_color, 3) over (partition by ticker order by [date]) as
color_3_back,
lag([date]) over (partition
by ticker order by [date]) as prev_date,
lead([date]) over (partition
by ticker order by [date]) as next_date,
lead([open]) over (partition
by ticker order by [date]) as next_open
from
dbo.First_HA_Dataset
),
-- layer 2: entry signals (red, red, red, green);
-- the
trade enters at the next trading day's open
-- (a
signal on a ticker's last bar has no next day and is skipped)
entry_signals as
(
select
ticker,
next_date
as entry_date,
next_open
as entry_price
from bars
where
ha_color = 'green'
and
color_1_back = 'red'
and
color_2_back = 'red'
and
color_3_back = 'red'
and next_date
is not null
),
-- layer 3: exit signals (red, red); the pair's first red
is prev_date,
-- and the
trade exits at the next trading day's open
exit_signals as
(
select
ticker,
prev_date
as first_red_date,
[date] as exit_signal_date,
next_date
as exit_date,
next_open
as exit_price
from bars
where
ha_color = 'red'
and
color_1_back = 'red'
),
-- layer 4: pair each entry with the first exit signal
for the same ticker
-- whose
two reds both fall on or after the entry date
trades as
(
select
e.ticker,
e.entry_date,
e.entry_price,
x.exit_date,
x.exit_price
from
entry_signals e
outer apply
(
select top
1 xs.exit_date, xs.exit_price
from
exit_signals xs
where
xs.ticker = e.ticker
and
xs.first_red_date >= e.entry_date
order by
xs.exit_signal_date
) x
where
e.entry_date >= @start_date
)
-- final layer: per-trade and cumulative percent results,
materialized
-- (a trade still open at the end of the data shows null
exit values
-- and does not
change the cumulative total)
select
ticker,
entry_date,
entry_price,
exit_date,
exit_price,
cast((exit_price - entry_price) / entry_price * 100 as decimal(18,4)) as
trade_pct,
cast(sum((exit_price - entry_price) / entry_price * 100)
over
(partition by ticker order by entry_date
rows unbounded preceding) as decimal(18,4)) as cum_pct
into dbo.First_HA_Trades
from trades;
-- natural primary key on (ticker, entry_date)
alter table dbo.First_HA_Trades alter column ticker varchar(20) not null;
alter table dbo.First_HA_Trades alter column entry_date
date not null;
alter table dbo.First_HA_Trades
add constraint
pk_First_HA_Trades primary key clustered (ticker, entry_date);
go
-- display the trades
select *
from dbo.First_HA_Trades
order by ticker, entry_date;
---------------------------------------------------------
-- summary by ticker
-- trades counts
closed trades only; count(trade_pct) skips the
-- null
trade_pct of a trade still open at the end of the data;
-- total_pct_change
sums the percent change of the closed trades
-- and matches
each ticker's last cum_pct value;
-- spy is listed
last as the benchmark; the other tickers are
-- listed
alphabetically
---------------------------------------------------------
select
ticker,
count(trade_pct) as trades,
cast(sum(trade_pct) as decimal(10,2)) as total_pct_change
from dbo.First_HA_Trades
group by ticker
order by case when ticker = 'SPY' then 1 else 0 end, ticker;
The next pair of screenshots show the first and last five rows from the first result set for the DELL ticker. Recall that the first result set is a trades ledger with ticker, entry date, entry price, exit date, and exit price along trade percentage return and cumulative percentage return. This pair of screenshots makes it easy to determine
- the initial entry date and price for the DELL trade was on March 9, 2017 for a price of $17.73, and
- the final exit date and price for the DELL trade was on September 30, 2026 for a price of $546.48.
The following pair of screenshots show comparable results
for the SPY ticker, which is the benchmark for the other eight tickers tracked
in this post.
The final screenshot in this post shows the trade count and total percentage change for each ticker covered in this post.
- All eight comparison tickers have a larger total percentage change than the SPY benchmark. The percentage values in the following display show percentage change achieved by the model.
- Also, four of the comparison tickers (AMD, DELL, MU, and NVDA) have total percentage change values in excess of 200 percentage points versus a total percentage change value of just 31.69 for the SPY ticker
- The relative performance chart at the end of the “A SQL Server Dynamic Ingestion Script for Downloaded CSV Files” section shows buy-and-hold price changes for the same tickers and timeframe. Those values are not directly comparable to the model’s totals, because buy-and-hold compounds one position held throughout, while the model sums the percent changes of separate fixed-amount trades. Even so, SPY ranks last under both measures.
- From the trades column below, you can also see that the 3 reds + 1 green in, 2 reds out model generates fewer trades for the SPY ticker than any of the remaining eight tickers. While the differences are not nearly as large as for total percentage change values, the results still show a tendency for the SPY ticker, which is a widely followed benchmark ticker, to be more unresponsive to the model.
This post demonstrates the practical application of Heikin Ashi candle colors for identifying trend reversals across a nearly ten-year evaluation window spanning from January 2017 through September 2026. The total percentage change finding for the 3 reds + 1 green in, 2 reds out model returned distinctly different total percentage return values for the eight actively traded tickers than the SPY benchmark ticker. A similar kind of distinction was also found for total trades by ticker and even for the rank order of buy-and-hold percentage change by ticker.
The 3 reds + 1 green in, 2 reds out model examined in this post illustrates only one of the many ways of using Heikin Ashi candle values to estimate worthy entry and exit trade points for a set of tickers. This model focuses on an adaptation of the popular “buy the dip” model that is configured for implementation with Heikin Ashi candles and underlying OHLC values.
If you decide that you wish to experiment with your own customized Heikin Ashi model or even just reuse the model covered in this post with a different set of tickers, you are likely to find some of the scripts in this post helpful. For that reason, all scripts referenced in this post appear in the body of the post.
I close by inviting you to leave comments with your suggestions for using Heikin Ashi candles for modeling entry and exit dates and prices for stock tickers in different ways than covered in this post.














Comments
Post a Comment