Our Head of Design learned Indie in two days — with MCP, a few (okay, ten) quick syncs with the Indie team, and the docs. This guide is what came out of it: the Halloween Effect Indicator, and the first steps to building your own.
A Bit of History
The Halloween Strategy is a classic seasonality pattern built on one long-standing observation: stock markets tend to perform better from October 31 (Halloween) to May 1 than during the summer months.
It’s as simple as a pumpkin — buy on Halloween, sell on May 1, then spend the summer doing literally anything else. Despite its simplicity, the pattern still shows up across markets. Research finds the “Halloween Effect” in 36 out of 37 developed and emerging economies.
How Should Our Indicator Look?
Before writing a single line of code, let’s decide what we want to see on the chart:
Let's start by creating the basic indicator structure and adding visual period separation. In Indie, any indicator begins with the @indicator decorator and a class inheriting from MainContext:
# indie:lang_version = 5from indie import indicator, color, plot, MainContext, param@indicator('🎃 Halloween Strategy', overlay_main_pane=True)# @plot.background decorators define background colors for different periods@plot.background(color=color.ORANGE(0.3), title='Winter Period Background')@plot.background(color=color.NAVY(0.3), title='Summer Period Background')class Main(MainContext):def __init__(self):...def calc(self):...
Key Indie Concepts:
@indicator — A decorator that turns a class into an indicator. The parameter overlay_main_pane=True means the indicator is drawn over the main price chart.@plot.background — A decorator for defining visualization elements. We're creating two background layers: orange for winter (when holding position) and navy for summer (when resting).MainContext — The base class for all indicators, providing access to market data (prices, time, volumes).from datetime import datetimefrom indie import MutSeriesdef calc(self):# The calc() method is called for each bar on the charttimestamp = self.time[0] # Get the current bar's timestampdt = datetime.utcfromtimestamp(timestamp)month = dt.monthday = dt.day# MutSeries - mutable data series, one of Indie's key structuresis_winter = MutSeries[bool].new(False)# Logic for determining winter period# Winter: from October 31 to May 1if month < 5: # January-Aprilis_winter[0] = Trueelif month >= 5 and month < 10: # May-Septemberis_winter[0] = Falseelif month == 10:if day < 31: # October before the 31stis_winter[0] = Falseelse: # October the 31stis_winter[0] = Trueelse: # November-Decemberis_winter[0] = True# Return the appropriate background colorif is_winter[0]:return plot.Background(), plot.Background(color=color.TRANSPARENT)return plot.Background(color=color.TRANSPARENT), plot.Background()
The MutSeries Idea
MutSeries is a mutable data series in Indie. Think of it as an array where index [0] is the current bar, [1] is the previous one, and so on. This makes it easy to work with historical data and create indicators that "remember" previous values.
Now that we have beautiful colored zones, let's add buy and sell signals. For this, we need to detect period transitions:
from indie.drawings import LabelAbs, AbsolutePosition, callout_positiondef calc(self):# ... (previous period determination code) ...# Detect transitions between periodsis_winter_start = is_winter[0] and not is_winter[1] # Winter startis_winter_end = not is_winter[0] and is_winter[1] # Winter endif is_winter_start:# Draw buy markerself.chart.draw(LabelAbs('Buy',AbsolutePosition(self.time[0], self.low[0]),bg_color=color.NAVY,text_color=color.WHITE,font_size=11,callout_position=callout_position.BOTTOM_LEFT,))if is_winter_end:# Draw sell markerself.chart.draw(LabelAbs('Sell',AbsolutePosition(self.time[0], self.high[0]),bg_color=color.ORANGE,text_color=color.WHITE,font_size=11,callout_position=callout_position.TOP_RIGHT,))
Key Non-Series Drawing Concepts in Indie:
LabelAbs — A label with absolute positioning on the chart. It's anchored to a specific point (time, price).AbsolutePosition — Defines the exact position on the chart. We place "Buy" at the bar's low and "Sell" at the high for better visibility.callout_position — The direction of the label's "tail." This small but important detail improves chart readability!Before calculating statistics, let's think like experienced traders: which metrics are truly important for evaluating a strategy?
My Wishlist
How Will We Calculate?
Now let's turn our plan into code. Add necessary variables to the constructor:
from indie import Optionaldef __init__(self):# Trading variablesself._position = 0self._open_price: Optional[float] = Noneself._total_trades = 0self._win_trades = 0# For Profit Factor calculationself._total_profit = 0.0self._total_loss = 0.0# For Sharpe Ratio calculationself._trades_pnl_list: list[float] = []# For Maximum Drawdown calculationself._initial_equity = 100.0self._current_equity = 100.0self._peak_equity = 100.0self._max_drawdown = 0.0
Now add calculation logic when opening and closing positions:
from indie.math import divide # Safe division (protection from division by 0)def calc(self):# ... (period determination code) ...if is_winter_start:if self._open_price is None:self._open_price = self.close[0] # Remember entry priceself._position += 1if is_winter_end:if self._open_price is not None:# Calculate current trade P&Ltrade_pnl_percent = (divide(self.close[0], self._open_price.value()) - 1.0) * 100.0# Save for Sharpe calculationself._trades_pnl_list.append(trade_pnl_percent)# Update equity with compoundingself._current_equity = self._current_equity * (1.0 + trade_pnl_percent / 100.0)# Update Maximum Drawdownif self._current_equity > self._peak_equity:self._peak_equity = self._current_equitycurrent_dd = divide(self._peak_equity - self._current_equity, self._peak_equity) * 100.0if current_dd > self._max_drawdown:self._max_drawdown = current_dd# Separate profitable and losing tradesif trade_pnl_percent > 0.0:self._win_trades += 1self._total_profit += trade_pnl_percentelif trade_pnl_percent < 0.0:self._total_loss += abs(trade_pnl_percent)self._total_trades += 1self._open_price = Noneself._position -= 1
Important Concept — Optional in Indie:
The Optional[float] type means the variable can contain either a float value or None. This helps track state — whether we have an open position or not. The .value() method extracts the value from Optional.
The final touch — let's create an informative statistics panel. We'll use LabelRel for relative positioning:
from statistics import mean, stdevfrom indie.drawings import LabelRel, RelativePosition, vertical_anchor, horizontal_anchordef __init__(self):# ... (previous code) ...# Create label for statistics in the bottom-right cornerself._stats_label = LabelRel('Stats text',position=RelativePosition(vertical_anchor=vertical_anchor.BOTTOM,horizontal_anchor=horizontal_anchor.RIGHT,top_bottom_ratio=0.98, # 98% from top (i.e., almost at the bottom)left_right_ratio=0.98, # 98% from left edge (i.e., almost at right)),bg_color=color.BLACK,text_color=color.WHITE,font_size=10)def calc(self):# ... (all previous code) ...# Calculate final metricsopen_pnl = 0.0if self._position > 0 and self._open_price is not None:open_pnl = (divide(self.close[0], self._open_price.value()) - 1.0) * 100.0win_rate = divide(self._win_trades, self._total_trades, 0) * 100.0total_pnl_real = self._current_equity - self._initial_equityprofit_factor = divide(self._total_profit, self._total_loss, 0)# Sharpe Ratiosharpe_ratio = 0.0if len(self._trades_pnl_list) > 1:avg_return = mean(self._trades_pnl_list)std_return = stdev(self._trades_pnl_list)sharpe_ratio = divide(avg_return, std_return, 0)# Format statistics textstats_text = ('Total P&L: ' + str(round(total_pnl_real, 2)) + '%\n' +'Win Rate: ' + str(round(win_rate, 2)) + '%\n' +'Trades: ' + str(self._total_trades) + '\n' +'Profit F: ' + str(round(profit_factor, 2)) + '\n' +'Sharpe: ' + str(round(sharpe_ratio, 2)) + '\n' +'Max DD: ' + str(round(self._max_drawdown, 2)) + '%\n' +'---\n' +'CURRENT PERIOD\n' +'Open P&L: ' + str(round(open_pnl, 2)) + '%')self._stats_label.text = stats_textself.chart.draw(self._stats_label)
Relative Positioning Concept:
LabelRel with RelativePosition allows placing elements relative to chart boundaries rather than anchoring to specific prices. This is perfect for information panels — they always stay in place regardless of chart zoom!
What if we want to test the strategy with different dates? Or change font size? Indie provides an elegant solution — @param decorators:
@indicator('🎃 Halloween Strategy', overlay_main_pane=True)@param.int('winter_start_month', default=10, min=1, max=12, title='Winter Period - Start Month')@param.int('winter_start_day', default=31, min=1, max=31, title='Winter Period - Start Day')@param.int('summer_start_month', default=5, min=1, max=12, title='Summer Period - Start Month')@param.int('summer_start_day', default=1, min=1, max=31, title='Summer Period - Start Day')@param.float('initial_equity', default=100.0, min=1.0, max=1000000.0, title='Initial Equity (%)')...class Main(MainContext):def __init__(self, initial_equity):# Now all parameters are customizable!...self._initial_equity = initial_equity...def calc(self, winter_start_month, winter_start_day, summer_start_month, summer_start_day):...if month < summer_start_month:...
@param decorators automatically create a user interface for configuring indicator parameters. Users can change dates, initial capital, and visual settings directly from the trading platform interface!
Congrats — you’ve just gone from idea to a fully working Halloween Strategy indicator.
This one doesn’t just flash Buy/Sell signals — it measures, visualizes, and tells you if the story actually holds up.
Here’s what you picked up along the way:
@indicator, MainContext, and the calc() loop.MutSeries for time series, Optional for flexible states.@plot.background, markers with LabelAbs and LabelRel.@param decorators for user-editable settings.indie.math.divide.The Halloween Strategy is a great reminder that even a simple seasonal rule can turn into a full analytical tool — when built with the right logic and structure. And Indie gives you exactly that: a way to turn trading ideas into something testable, visual, and real.
Just remember: even if it worked in the past, there are no guarantees for the future.
But now you’ve got the tools to test every myth the market ever told.
Need help? MCP, Discord, and the Indie dev team have your back:
Happy Halloween Trading 🎃


Be the first to comment
Publish your first comment to unleash the wisdom of crowd.