Identifying swings (also known as pivots) can be very useful for various trading styles. The obvious example is trend following where you might want to enter the trend on a dip or adjust stops to just below the most recent swing. The post will provide code for implementing a basic swing indicator.
Note: This post assumes a certain level of familiarity with Backtrader and Python. I assume you know the basics and how to initialize an indicator in a strategy. If you are new to Backtrader, you may find my getting started series which starts with Getting Setup: Python and Backtrader.
Swing Indicator
As the name suggests, our swing indicator is going to produce a signal when it determines a swing happened. Notice the past tense language? Given the nature of swings, we can only identify a swing happened “after the fact”. We need to wait a some time for more candles to appear before we can be confident is calling it a swing. With this in mind the swing indicator needs to be flexible enough so that the “sensitivity” can be altered according to personal preference.
Here is what the indicator will do:
- Analyze a historical candle and it’s surrounding candles to determine if a swing happened.
- The indicator will consider it a swing if:
- The candle being checked is the highest high for the period length both before and after it.
- The candle being checked is the lowest low for the period length both before and after it.
- When identified, create a signal:
- Swing High = 1
- Swing Low = -1
- Note: You may want to flip these around. I will discuss this further in the results section.
- A second “Indicator” line will be used to mark when the swing happened (i.e. not when it was signaled). This is useful for peace of mind and verification that the indicator is working as expected.
The Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
class SwingInd(bt.Indicator): ''' A Simple swing indicator that measures swings (the lowest/highest value) within a given time period. ''' lines = ('swings', 'signal') params = (('period',7),) def __init__(self): #Set the swing range - The number of bars before and after the swing #needed to identify a swing self.swing_range = (self.p.period * 2) + 1 self.addminperiod(self.swing_range) def next(self): #Get the highs/lows for the period highs = self.data.high.get(size=self.swing_range) lows = self.data.low.get(size=self.swing_range) #check the bar in the middle of the range and check if greater than rest if highs.pop(self.p.period) > max(highs): self.lines.swings[-self.p.period] = 1 #add new swing self.lines.signal[0] = 1 #give a signal elif lows.pop(self.p.period) < min(lows): self.lines.swings[-self.p.period] = -1 #add new swing self.lines.signal[0] = -1 #give a signal else: self.lines.swings[-self.p.period] = 0 self.lines.signal[0] = 0 |
Code Commentary
As you can see, this indicator is not very complex. However a couple of lines are worth explaining further:
1 |
self.swing_range = (self.p.period * 2) + 1 |
We need to analyze the historical data both BEFORE and AFTER the candle being checked. So the whole range is double the period we select. The + 1, ensures that we have an equal amount of candles on either side of the candle that is analyzed. Next we need to get the data:
1 2 |
highs = self.data.high.get(size=self.swing_range) lows = self.data.low.get(size=self.swing_range) |
It is worth mentioning that because of how Backtrader indexes data in a line, we cannot slice the data in the same way we usually would in Python. For this reason we need to use a special get() method from the framework to return the data we want to check.
Finally we just pop() the candle being analyzed from the list and compare it against the rest of the candles that remain in the list. If it is has a higher high or lower low than the rest of the candles then we have a swing!
Result
The first result shows a period set as 7.
Note: The red line is the swing line and the blue line is the signal line. You can only act on the signal line as the swing line is altered after the fact.
With such a short period we can see a lot of signals are generated. Lets zoom in for a closer look:
Zooming in we can see that we catch the good swings and generate a signal early enough to catch the following movement. The downside is that we also catch some questionable, insignificant swings.
Now we are looking at a chart in more detail, this would be a good time to mention why you might want to change is the signal values. I have it so -1 is a swing low and 1 is a swing high. If you want to use this indicator with a signals only script, it would make more sense to flip these around. This would mean a swing high is -1 and would indicate a short signal. Conversely, a swing low would equal 1, indicating a long signal.
Finally lets compare with a period of 14:
As you can see in the image, we do not have the issue of flagging low quality swings. However with such a long period set, the signals we do generate, arrive quite some time after the swing happened.
So in conclusion, there is a trade off between balancing the need to generate signals quickly against the need for only signaling significant swings. If you leave it too long, the price may not have much further to move before retracing. If you set it to too short, you can end up with too much noise. I personally think that a shorter period may suit those who are looking to capitalize on the price movement immediately following a swing. On the other side of the table, a longer period may suit those who just want to move stops after a significant swing and do not want to be caught out by the noise.
Hi,
I think lines should be like :
lines = (‘signal’,’swings’)
Because when used as indicator in a strategy, using swings process data from future:
In normal mode swings are preloaded but this is a cheat, and cause this indicator to have crazy perf
In live mode, swings return Nan because swings[0] is not loaded yet.
Hi Theo!
Thanks for the comment. Are you using this indicator in live mode?
The indicator has an
addminperiod()
call during__init__()
this should ensure that the indicator does not do anything until it has enough data to perform the calculations. Also, it should not use any data from the future.Are you seeing it do something else? What is happening during the crazy performance? Could it be that you are backfilling at the start and entering trades without checking whether the data feed is live? I have done this before and it results in lots of trades being fired off against the historical data before the live data kicks in.
Also, I am not sure how switching the line order will resolve the problem you are seeing. Maybe you can educate me? If there is an issue, I would be happy to make the correction and credit you for the pointer!
Thanks and Merry Christmas (If you celebrate it!)
Hi Daniel,
many thanks for your well organized blogs…
However, may I kindly ask you to explain this your comment:
#check the bar in the middle of the range and check if greater than rest?
Why is this in the middle of the range, if pop() is an inbuilt function in Python that removes and returns last value from the list or the given index value?
(if highs.pop(self.p.period) > max(highs):)
Many thanks,
Vaclav
How come the script does not work on tradingview? I have tried it several times.