While I was developing my trading algo, I wanted to calculate different technical indicators, most of which depend on previous data points. In order to do that in SQL Server you have a couple options, and this is one of them.
There are two key points that make this “quirky update” work.
- You MUST have a clustered index on the table that represents the expected sort order of the data. In this case is was the tick data ascending.
- You MUST use
OPTION (MAXDOP 1)option on the query so SQL Server doesn’t try to parallelize the update.
Setup
Below we create a staging table to house the technical indicators, the primary key is the clustered index, and tick data is inserted into the table in sorted order. You could also have the clustered index on the [Time] column as well.
--DROP TABLE [stage].[Meta]CREATE TABLE [stage].[Meta] ( [RowIdx] BIGINT IDENTITY PRIMARY KEY, [Id] UNIQUEIDENTIFIER, [InstrumentId] INT, [Pip] NUMERIC(10, 5), [Time] DATETIME2, [Open] NUMERIC(10, 5), [High] NUMERIC(10, 5), [Low] NUMERIC(10, 5), [Close] NUMERIC(10, 5), [Volume] INT);In the following examples take note of the usage of the variables that retain the previous calculation result and how they get used when updating the calculation for the next row. This essentially causes the same behavior you would get if you had used a SQL CURSOR.
Example — Heikin Ashi Candlestick
/* indicator columnsALTER TABLE [stage].[Meta]ADD [HaOpen] NUMERIC(10, 5), [HaHigh] NUMERIC(10, 5), [HaLow] NUMERIC(10, 5), [HaClose] NUMERIC(10, 5)--*/
-- calculate heikin ashi candles-- NOTE: variable ordering matters here---------------------------------------------------------------------------------------------------DECLARE @HaOpen NUMERIC(10, 5) = 0, @HaClose NUMERIC(10, 5) = 0, @PrevHaOpen NUMERIC(10, 5) = 0, @PrevHaClose NUMERIC(10, 5) = 0
-- quirky update --UPDATE [stage].[Meta]SET @HaClose = ([Open] + [High] + [Low] + [Close]) / 4.0, [HaClose] = @HaClose,
@HaOpen = CASE WHEN [RowIdx] = 1 THEN ([Open] + [Close]) / 2.0 ELSE (@PrevHaOpen + @PrevHaClose) / 2.0 END, [HaOpen] = @HaOpen,
[HaHigh] = (SELECT MAX([val]) FROM ( VALUES ([High]), ([HaOpen]), ([HaClose]) ) T([val])), [HaLow] = (SELECT MIN([val]) FROM ( VALUES ([Low]), ([HaOpen]), ([HaClose]) ) T([val])),
@PrevHaClose = @HaClose, @PrevHaOpen = @HaOpenOPTION (MAXDOP 1);Example — Exponential Moving Average
/* indicator columnsALTER TABLE [stage].[Meta]ADD [EMA012] NUMERIC(10, 5), [EMA026] NUMERIC(10, 5)--*/
-- calculate ema's-- NOTE: depends on sma being calculated first----------------------------------------------------------------------------------------------------- quirky update --DECLARE @EMA012 NUMERIC(10, 5) = 0, @EMA026 NUMERIC(10, 5) = 0
UPDATE [stage].[Meta]SET @EMA012 = CASE WHEN [RowIdx] = 1 THEN [SMA012] ELSE ([Close] - @EMA012) * (2 / ((12 + 1) * 1.0)) + @EMA012 END, [EMA012] = @EMA012,
@EMA026 = CASE WHEN [RowIdx] = 1 THEN [SMA026] ELSE ([Close] - @EMA026) * (2 / ((26 + 1) * 1.0)) + @EMA026 END, [EMA026] = @EMA026OPTION (MAXDOP 1);Summary
I first came across this when looking for a better way to perform the update without having all of the ceremony that comes with the CURSOR. You can find the original article below and their 10 rules on using this technique.
- CLUSTERED INDEX MUST BE PRESENT IN THE CORRECT ORDER
- PARALLELISM MUST BE PREVENTED: MUST include OPTION (MAXDOP 1)
- DON’T WORK AGAINST PARTITIONED STRUCTURES
- USE THE TABLOCKX HINT
- DO NOT USE JOINS
- YOU MUST HAVE AN “ANCHOR” COLUMN
- DO NOT USE ORDER BY
- DO NOT USE INDEX HINTS TO TRY TO FORCE ORDER
- GET IT RIGHT
- TEST!
~ SK