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 = 5
from indie import indicator
from indie.algorithms import SinceHighest
from indie.drawings import LabelAbs, AbsolutePosition
@indicator('Simple Pivots', overlay_main_pane=True)
def Main(self):
# Find how many bars have passed since the last high
since_high = SinceHighest.new(self.high, 10)
# If since_high[0] == 0, the current bar is a new high
if since_high[0] == 0:
# Create a label with the current price
label = LabelAbs(
str(self.high[0]), # Label text
AbsolutePosition(self.time[0], self.high[0]) # Position on chart
)
# Draw the label
self.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 = 5
from indie import indicator, color
from indie.algorithms import SinceHighest, SinceLowest
from 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 high
if 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 highs
bg_color=color.BLACK(0), # Transparent background
callout_position=callout_position.TOP_RIGHT
)
self.chart.draw(label)
# New low
if 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 lows
bg_color=color.BLACK(0),
callout_position=callout_position.BOTTOM_LEFT
)
self.chart.draw(label)
What's new:
SinceLowest
callout_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 = 5
from indie import indicator, param, color, MainContext, Optional
from indie.algorithms import SinceHighest, SinceLowest
from 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] = None
self._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 = length
def calc(self) -> None:
# Search for new extremes
since_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 and
self.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 and
self.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