Exponential moving average in T-SQL

Exponential moving averages are similar to weighted moving averages in that they assign less weight to changes long ago, and more weight to recent changes. Weighted moving averages are linear, but exponential moving averages are exponential. That is, the weight can be expressed as a curve:

Weights in EMA
Weights in EMA

There is a great way to calculate exponential moving averages in T-SQL using an undocumented feature about variables and running totals in SQL Server. In this blog post I will show how to use that method to calculate exponential moving average in T-SQL, but I will also present a method that is using standard features in SQL Server. Unfortunately, that means using a loop.

In the examples I will calculate a 9 days exponential moving average. The examples use the database TAdb. A script to create TAdb can be found here.

Exponential Moving Average (EMA): Running Totals Method

The theory behind the running total features in updates is described in detail by Jeff Moden in his article Solving the Running Total and Ordinal Rank Problems.

Other resources that describe using this method to calculate EMA are the blog post Calculating Moving Averages with T-SQL by Gabriel Priester and the forum post Exponential Moving Average Challenge, both on SQL Server Central.

Basically, in T-SQL you can update variables as well as columns in an update statement. The updates are done row by row internally by SQL Server. This row by row behavior is what makes calculating a running total possible.

This example shows how it works:

DECLARE @TBL_RT TABLE
	(
		ColumnToSum INT,
		ColumnRunningTotal INT NULL
	)

INSERT @TBL_RT (ColumnToSum) VALUES (1),(2),(3)

DECLARE @varRunningTotal INT = 0

UPDATE
	@TBL_RT
SET
	@varRunningTotal = ColumnRunningTotal = ColumnToSum + @varRunningTotal

SELECT * FROM @TBL_RT

ColumnToSum ColumnRunningTotal
----------- ------------------
1           1
2           3
3           6

Note that “ColumnRunningTotal” is a running total of “ColumnToSum”.

Using this method we can calculate EMA9 with this T-SQL:

IF OBJECT_ID('tempdb..#TBL_EMA9_RT') IS NOT NULL BEGIN
	DROP TABLE #TBL_EMA9_RT
END

SELECT *, CAST(NULL AS FLOAT) AS EMA9 INTO #TBL_EMA9_RT FROM dbo.Quotes

CREATE UNIQUE CLUSTERED INDEX EMA9_IDX_RT ON #TBL_EMA9_RT (StockId, QuoteId)

IF OBJECT_ID('tempdb..#TBL_START_AVG') IS NOT NULL BEGIN
	DROP TABLE #TBL_START_AVG
END

SELECT StockId, AVG(QuoteClose) AS Start_Avg INTO #TBL_START_AVG FROM dbo.Quotes WHERE QuoteId <= 9 GROUP BY StockId

DECLARE @C FLOAT = 2.0 / (1 + 9), @EMA9 FLOAT

UPDATE
	T1
SET
	@EMA9 =
		CASE
			WHEN QuoteId = 9 then T2.Start_Avg
			WHEN QuoteId > 9 then T1.QuoteClose * @C + @EMA9 * (1 - @C)
		END
	,EMA9 = @EMA9 
FROM
	#TBL_EMA9_RT T1
JOIN
	#TBL_START_AVG T2
ON
	T1.StockId = T2.StockId
OPTION (MAXDOP 1)

SELECT StockId, QuoteId, QuoteDay, QuoteClose, CAST(EMA9 AS NUMERIC(10,2)) AS EMA9 FROM #TBL_EMA9_RT
EMA9 Results
EMA9 Results

The calculation of EMA is rather simple. We use the current row and the previous, but with more weight to the current row. The weight is calculated by the formula 2/(1+9), where “9” is the parameter for the length of the EMA. To calculate EMA9 for row 10 above, the calculation is:

SELECT (29.94*2.0/(1+9)) +  30.17 * (1 - 2.0/(1+9))
30.124

In this case the current row gets 20% of the weight (2/(1+9)=0.2) and the previous row gets 80% of the weight (1-2/(1+9)=0.8).

You find this calculation in the statement above in the CASE statement:

WHEN QuoteId > 9 then T1.QuoteClose * @C + @EMA9 * (1 - @C)

Exponential Moving Average (EMA): Looping Method

As far as I know, except for the running totals method outlined above, there is no way to calculate EMA using a set based SQL statement. Therefore, the T-SQL below is using a while loop to calculate EMA9:

IF OBJECT_ID('tempdb..#TBL_EMA9_LOOP') IS NOT NULL BEGIN
	DROP TABLE #TBL_EMA9_LOOP
END

SELECT *, CAST(NULL AS FLOAT) AS EMA9 INTO #TBL_EMA9_LOOP FROM dbo.Quotes

CREATE UNIQUE CLUSTERED INDEX EMA9_IDX ON #TBL_EMA9_LOOP (StockId, QuoteId)

DECLARE @StockId INT = 1, @QuoteId INT, @QuoteIdMax INT, @StartAvg FLOAT, @C FLOAT = 2.0 / (1 + 9), @EMA9 FLOAT

WHILE @StockId <= 2 BEGIN
	SELECT @QuoteId = 1, @QuoteIdMax = MAX(QuoteId) FROM dbo.Quotes WHERE StockId = @StockId
	SELECT @StartAvg = AVG(QuoteClose) FROM dbo.Quotes WHERE StockId = @StockId AND QuoteId <= 9

	WHILE @QuoteId <= @QuoteIdMax BEGIN
			UPDATE
				T0
			SET
				EMA9 =
					CASE
						WHEN @QuoteId = 9 THEN @StartAvg
						WHEN @QuoteId > 9 THEN (T0.QuoteClose * @C) + T1.EMA9 * (1.0 - @C)
					END
			FROM
				#TBL_EMA9_LOOP T0
			JOIN
				#TBL_EMA9_LOOP T1
			ON
				T0.StockId = T1.StockId
			AND
				T0.QuoteId - 1 = T1.QuoteId
			WHERE
				T0.StockId = @StockId
			AND
				T0.QuoteId = @QuoteId
			
			SELECT @QuoteId = @QuoteId + 1
	END

	SELECT @StockId = @StockId + 1
END

SELECT StockId, QuoteId, QuoteDay, QuoteClose, CAST(EMA9 AS NUMERIC(10,2)) AS EMA9 FROM #TBL_EMA9_LOOP

The results are the same as in the running totals example above.

Performance

As expected, the set based running totals version is way faster than the loop version. On my machine the set based solution was about 300 ms, compared to about 1200 with the loop version. The loop version is more conforming to SQL standards however. So the choice between the methods depends on what’s most important for you, performance or standards.

Usage

The exponential moving average can be used in trend analysis, as with the other types of moving averages, Simple Moving Average (SMA) and Weighted moving average (WMA).

There are also other calculations in technical analysis that uses the EMA, MACD for instance.

[blue_box]This blog post is part of a serie about technical analysis, TA, in SQL Server. See the other posts here.[/blue_box]

Tomas Lind

Tomas Lind - Consulting services as SQL Server DBA and Database Developer at High Coast Database Solutions AB.

This Post Has 4 Comments

  1. Hi,

    Found this very usefull. Is there a way to do an initial takeon and then only process the delta? So, I want to load 2000 days worth of history and then only load say the last 7 days worth of data and then only perfrom cals for the new data and merge\insert into my master table.

    Cheers

    1. Hi, that should be possible since the resulting EMA is stored in its own column.

      Regards
      Tomas

  2. Kaeman

    Nolan;
    I the way I do it is kind of thinking outside the box. I calculate the 20 EMA’s over 7k stock tickers every day in about 15 minutes. I put the results on my website stocksreally.com. You don’t need a loop or anything…

Leave a Reply