Construct Bollinger Band Trend with Elasticsearch
If you have read my previous article “Calculating Bollinger Band Width Using Elasticsearch”, you may want to build the Bollinger Band Trend indicator (BBTrend) as well. BBTrend is a relatively new indicator developed by John Bollinger after the announcement of Bollinger Band Width (BBW). It can reflect the trend of the target stock with both strength and direction, making it a valuable reference for trading. When the BBTrend reading is higher than zero, it may denote a bullish market, and a bearish market vice versa. The degree above or below zero determines the strength or momentum behind the trend. BBTrend involves two Bollinger Bands, long-term and short-term. The default period is 20 and 50.
You may refer to my previous article (Calculate Bollinger Band Width Through Elasticsearch) to find out how to calculate Bollinger Band (BB) by using Elasticsearch. The BBTrend is calculated as the following equation.
In the above formula, the variables denote as:
tp: the typical price, where tp = (high + low + close)/3.SMA: simple moving averageshort: the sliding window of the short-term period SMA, 20 is used here.long: the sliding window of the long-term period SMA, 50 is used here.abs: absolute value BBUshort: short period BB’s upper bandBBLshort: short period BB’s lower bandBBUlong: long period BB’s upper bandBBLlong: long period BB’s lower band
In this article, we use the commission-free exchange traded funds (ETF) as samples and focus on Elasticsearch as a tool for analysis. The following example randomly picks “Fidelity Low Volatility Factor ETF”. Its ticker symbol is FDLO. The data is selected from the time range between December 15, 2020 and May 31, 2021 provided by IEX, Investors Exchange. Suppose there is an Elasticsearch index populated with data, and its data mapping used is the same as the previous paper. The step-by-step operations and the corresponding REST API request body is shown as follows:
1. Collect all relevant documents through the search operation
Use a “bool” query with a “must” clause to collect documents with the symbol FDLO and the trading date between March 1, 2020 and May 31, 2021. Due to the computation of the 50 days moving average, one and half month’s additional data has been adjusted (from December 15, 2020, to Feburary 28, 2020).
{
"query":{
"bool":{
"must": [
{"range": {"date": {"gte":"2020-12-15", "lte":"2021-05-31"}}},
{"term": {"symbol": "FDLO"}}
]
}
},
2. Calculate the daily typical value of the fund
Use a “date_histogram” aggregation, named BBI, with the parameter “field” as “date” and the parameter “interval” as “1d” to extract the prices of the fund each day. Then followed by a “scripted_metric” aggregation, named TP, to calculate the typical price.
"aggs" : {
"BBI" : {
"date_histogram" : {
"field" : "date",
"interval" : "1d",
"format" : "yyyy-MM-dd"
},
"aggs": {
"TP" : {
"scripted_metric":{
"init_script": "state.totals=[]",
"map_script": "state.totals.add((doc.high.value+doc.low.value+doc.close.value)/3)",
"combine_script": "double total=0; for (t in state.totals) {total += t} return total",
"reduce_script": "return states[0]"
}
},
3. Extract the date of the bucket
Because additional data is added, subsequent operations need to filter out the out-of-range part, so a “min” aggregation named “DateStr” is to get the date of the bucket. In Elasticsearch server, the date is stored in Epoch time. The time unit is milliseconds, and the time zone is UTC.
"DateStr": {
"min": { "field": "date"}
},
4. Select the buckets with more than 1 document
When a “moving_fn” aggregation is used to calculate moving average, the parameter “min_doc_count” of the “date_histogram” aggregation is restricted to 0. In order to filter out the empty buckets (non-trading days), a “bucket_selector” aggregation is used to select buckets with its document count greater than 0.
"STP": {
"bucket_selector": {
"buckets_path": {"count":"_count"},
"script": "params.count > 0"
}
},
5. Calculate the daily 20-day and 50-day simple moving average of the typical value
Use a “moving_fn” aggregation, named SMA20, with the parameter window as 20 and the parameter “buckets_path” as TP to calculate the 20-day SMA of the typical value. The SMA50 aggregation is done in the same way. SMA is calculated by using the unweighted average function (MovingFunctions.unweightedAvg).
"SMA20": {
"moving_fn" : {"script":"MovingFunctions.unweightedAvg(values)", "window":20, "buckets_path":"TP.value"}
},
"SMA50": {
"moving_fn" : {"script":"MovingFunctions.unweightedAvg(values)", "window":50, "buckets_path":"TP.value"}
},
6. Calculate the daily 20-day and 50-day standard deviation of the typical value
Use a “moving_fn” aggregation, named SD20, with the parameter window as 20 and the parameter “buckets_path” as TP to calculate the daily 20-day standard deviation. The SD50 is done in the same way. SD is calculated by using the standard deviation function (MovingFunctions.stdDev).
"SD20": {
"moving_fn": {"script":"MovingFunctions.stdDev(values, MovingFunctions.unweightedAvg(values))", "window":20, "buckets_path":"TP.value"}
},
"SD50": {
"moving_fn": {"script":"MovingFunctions.stdDev(values, MovingFunctions.unweightedAvg(values))", "window":50, "buckets_path":"TP.value"}
},
7. Calculate the daily BBU and BBL for 20-day and 50-day period
Use two “bucket_script” aggregations, named BBU20 and BBL20, with the parameter “buckets_path” to specify the results from SMA20 aggregation and SD20 aggregation. Then, the BBL20 and BBU20 are calculated from SMA20 with plus and minus the value of 2 times SD20. The BBL50 and BBU50 aggregations are done in the same way.
"BBU20": {
"bucket_script": {
"buckets_path": {
"SMA": "SMA20",
"SD": "SD20"
},
"script": "params.SMA + 2 * params.SD"
}
},
"BBL20": {
"bucket_script": {
"buckets_path": {
"SMA": "SMA20",
"SD": "SD20"
},
"script": "params.SMA - 2 * params.SD"
}
},
"BBU50": {
"bucket_script": {
"buckets_path": {
"SMA": "SMA50",
"SD": "SD50"
},
"script": "params.SMA + 2 * params.SD"
}
},
"BBL50": {
"bucket_script": {
"buckets_path": {
"SMA": "SMA50",
"SD": "SD50"
},
"script": "params.SMA - 2 * params.SD"
}
},
8. Calculate the Bollinger Band Trend
Use a “bucket_script” aggregation, named BBTrend, with the parameter “buckets_path” to specify the values of BBU20, BBL20, SMA20, BBU50 and BBL50 to calculate the BBTrend according to the previous equation.
"BBTrend": {
"bucket_script": {
"buckets_path": {
"BBU20": "BBU20",
"BBL20": "BBL20",
"SMA20": "SMA20",
"BBU50": "BBU50",
"BBL50": "BBL50"
},
"script": "(Math.abs(params.BBL20 - params.BBL50) - Math.abs(params.BBU20 - params.BBU50) ) / params.SMA20"
}
},
9. Identify whether the BBTrend value is within an uptrend or a downtrend.
a) Use a “derivative” aggregation named, BBTrendDiff, and the parameter “buckets_path” to specify the value of BBTrend to determine whether it is a increment or decrement from the previous timestamp.
"BBTrendDiff" :{
"derivative": {
"buckets_path": "BBTrend"
}
},
b) Use a “bucket_script” aggregation, named BBTrendType, with the parameter “buckets_path” to specify the values of BBTrend and BBTrendDiff to classify the type of the BBTrend value.
● Type 1 if it is a decrement and BBTrend < 0
● Type 2 if it is an increment and BBTrend < 0
● Type 3 if it is an increment and BBTrend > 0
● Type 4 if it is a decrement and BBTrend > 0
● Type 0 for other cases
"BBTrendType": {
"bucket_script": {
"buckets_path": {
"BBTrend": "BBTrend",
"BBTrendDiff": "BBTrendDiff"
},
"script": "(params.BBTrend > 0) ? (params.BBTrendDiff > 0 ? 3 : 4) : ((params.BBTrend < 0) ? (params.BBTrendDiff > 0 ? 2 : 1): 0)"
}
},
10. Filter out the additional documents for output
Use a “bucket_selector” aggregation, named SBBW, with the parameter “buckets_path” as “DateStr” to select the correct buckets specified in the “script” statement. The selection criterion is those buckets having the date on or after March 1, 2021 (the epoch time 1614556800000 is specified in milliseconds).
"SBBW": {
"bucket_selector": {
"buckets_path": {"DateStr":"DateStr"},
"script": "params.DateStr >= 1614556800000L"
}
}
}
},
"size": 0
}
11. After collecting the results, the 20-day BB and the 50-day BB with the typical value is plotted as the following figure.
12. The BBTrend is drawn with a bar graph, as shown in Figure 2. The bars of Type 0 to Type 4 are colored as white, red, orange, aqua, and blue, respectively.
According to the definition of BBTrend, it is quite easy to depict the shape in Figure 2 from Fig. 1. Basically, when the distance between BBL20 and BBL50 is greater than the distance between BBU20 and BBU50, BBTrend value will be positive. The greater the difference, the greater the driving force behind the trend. The 20-day BB in the second half of Fig. 1 is leaned to the upper part of the 50-day BB. Therefore, the corresponding BBTrend values in Fig. 2 are positive and larger. However, when we combine the typical value from Fig. 1 and try to explain whether it is an upward or a downward trend, it seems not that easy and straightforward. Regarding to the implementation used in this article with Elasticsearch, it shows seamlessly and is easy to understand. Readers can further refer to the open-source project on GitHub (Construct_Bollinger_Band_Trend_with_Elasticsearch).
Remarks:
I. Thanks to IEX (Investors Exchange) providing ETF data and also GitHub providing open-source project storage.
II. This article is based on a technical thought and does not constitute any investment advice. Readers must take their own responsibilities when using it.
III. There may still have errors in the article, and I urge readers to correct me.
IV. Those readers feel interests can refer to the book authored by the writer for all basic skills of Elasticsearch. “Advanced Elasticsearch 7.0”, August 2019, Packt, ISBN: 9781789957754.