Indie 5.4.0 introduced powerful drawings functionality — the ability to draw objects on charts that are not tied to time series. Let's step-by-step create an indicator to find local extremes (pivots) to explore these capabilities.
Drawings allow you to:
Unlike regular series, drawings remain on the chart and can be modified even on historical bars.
Let's start with a basic version that simply finds local highs:
# indie:lang_version = 5from indie import indicatorfrom indie.algorithms import SinceHighestfrom indie.drawings import LabelAbs, AbsolutePosition@indicator('Simple Pivots', overlay_main_pane=True)def Main(self):# Find how many bars have passed since the last highsince_high = SinceHighest.new(self.high, 10)# If since_high[0] == 0, the current bar is a new highif since_high[0] == 0:# Create a label with the current pricelabel = LabelAbs(str(self.high[0]), # Label textAbsolutePosition(self.time[0], self.high[0]) # Position on chart)# Draw the labelself.chart.draw(label)
What's happening:
SinceHighest.new(self.high, 10) looks for the highest value in the last 10 barsLabelAbs creates a text label at absolute chart coordinatesself.chart.draw() places the label on the chartThis code draws some labels on the chart, which works well. However, there's a problem: each time a new pivot is found, the previous one should be removed if it's too close to the new one. We'll come back to solving this later.
Now let's add local low detection and improve the appearance:
# indie:lang_version = 5from indie import indicator, colorfrom indie.algorithms import SinceHighest, SinceLowestfrom indie.drawings import LabelAbs, AbsolutePosition, callout_position@indicator('Pivots High/Low Basic', overlay_main_pane=True)def Main(self):since_high = SinceHighest.new(self.high, 10)since_low = SinceLowest.new(self.low, 10)# New highif since_high[0] == 0:label = LabelAbs(str(round(self.high[0], 2)),AbsolutePosition(self.time[0], self.high[0]),text_color=color.GREEN, # Green for highsbg_color=color.BLACK(0), # Transparent backgroundcallout_position=callout_position.TOP_RIGHT)self.chart.draw(label)# New lowif since_low[0] == 0:label = LabelAbs(str(round(self.low[0], 2)),AbsolutePosition(self.time[0], self.low[0]),text_color=color.RED, # Red for lowsbg_color=color.BLACK(0),callout_position=callout_position.BOTTOM_LEFT)self.chart.draw(label)
What's new:
SinceLowestcallout_position determines where the label is positioned relative to the point round() to format prices nicelyNow we’ve added low pivots and fine-tuned the label styles, which makes them look better. However, the issue mentioned at the end of the Step 1 chapter is still present. That’s why we now need to implement smarter label management.
Let's add a "memory" system to control when we create new labels vs update existing ones:
# indie:lang_version = 5from indie import indicator, param, color, MainContext, Optionalfrom indie.algorithms import SinceHighest, SinceLowestfrom indie.drawings import LabelAbs, AbsolutePosition, callout_position@indicator('Pivot Points High/Low', overlay_main_pane=True)@param.int('length', default=10, min=1, title='Lookback period')class Main(MainContext):def __init__(self, length):none_label: Optional[LabelAbs] = Noneself._high_pivot = self.new_var(none_label)self._low_pivot = self.new_var(none_label)self._high_pivot_bar = self.new_var(0)self._low_pivot_bar = self.new_var(0)self._length = lengthdef calc(self) -> None:# Search for new extremessince_high = SinceHighest.new(self.high, self._length + 1)since_low = SinceLowest.new(self.low, self._length + 1)if since_high[0] == 0:self._handle_high_pivot()if since_low[0] == 0:self._handle_low_pivot()def _handle_high_pivot(self) -> None:existing_pivot = self._high_pivot.get()if (existing_pivot is not None andself.bar_index - self._high_pivot_bar.get() <= self._length):# Update existing label (recent pivot)existing_pivot.value().position = AbsolutePosition(self.time[0], self.high[0])existing_pivot.value().text = str(round(self.high[0], 2))self.chart.draw(existing_pivot.value())self._high_pivot_bar.set(self.bar_index)else:# Create new label (no pivot or old pivot)new_label = LabelAbs(str(round(self.high[0], 2)),AbsolutePosition(self.time[0], self.high[0]),text_color=color.GREEN,bg_color=color.BLACK(0),callout_position=callout_position.TOP_RIGHT,font_size=11)self.chart.draw(new_label)self._high_pivot.set(new_label)self._high_pivot_bar.set(self.bar_index)def _handle_low_pivot(self) -> None:existing_pivot = self._low_pivot.get()if (existing_pivot is not None andself.bar_index - self._low_pivot_bar.get() <= self._length):# Update existing label (recent pivot)existing_pivot.value().position = AbsolutePosition(self.time[0], self.low[0])existing_pivot.value().text = str(round(self.low[0], 2))self.chart.draw(existing_pivot.value())self._low_pivot_bar.set(self.bar_index)else:# Create new label (no pivot or old pivot)new_label = LabelAbs(str(round(self.low[0], 2)),AbsolutePosition(self.time[0], self.low[0]),text_color=color.RED,bg_color=color.BLACK(0),callout_position=callout_position.BOTTOM_LEFT,font_size=11)self.chart.draw(new_label)self._low_pivot.set(new_label)self._low_pivot_bar.set(self.bar_index)
Key concepts:
self.new_var() creates variables that persist between calc() calls How it works:
Redundant labels are no longer being rendered on the chart. Great progress!
LabelAbs, LineSegment, etc.self.chart.draw(object)draw() again self.chart.erase(object)self.new_var() to store data between barsDrawings open up huge possibilities for creating interactive and informative indicators in Indie!
Comments