Picture this: late October, streets lined with pumpkins, kids getting ready for Halloween—and traders… opening long positions. Sounds like the start of a Wall Street horror flick? In reality, it’s one of the most recognizable seasonal strategies in finance: the Halloween Strategy, aka “Sell in May and Go Away.”
Here’s the twist. This indicator—and the article you’re reading—were built by our designer, who had never written Python before. He paired with Claude through our MCP server, using conversational prompts to scaffold Indie snippets, run them, fix errors, and iterate right on the chart.
In the sections that follow, we’ll turn the Halloween effect into a polished Indie indicator with color-coded periods, clean Buy/Sell markers, and a compact performance panel you can trust at a glance.
A Bit of History
The Halloween Strategy is an investment approach based on the historical observation that stock markets demonstrate better returns during the period from October 31st (Halloween) to May 1st than during the summer months. The strategy is as simple as a pumpkin: buy stocks on Halloween, sell on May 1st, and relax on the beach all summer!
The historical roots of this anomaly go back to the 18th century London Stock Exchange. The saying "Sell in May and go away, come back on St. Leger's Day" reflected British traders' habit of leaving for summer holidays, leaving markets with reduced liquidity.
Surprisingly, this simple strategy shows positive results in many world markets. Research confirms that the "Halloween effect" is observed in 36 out of 37 developed and emerging markets.
How Should Our Indicator Look?
Before writing code, let's dream about 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):...
Key Indie Concepts:
overlay_main_pane=True means the indicator is drawn over the main price chart.Now let's add logic to determine the current period:
def 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 day < 1: # May before the 1stis_winter[0] = Trueelif month >= 10: # October-Decemberif month == 10 and day < 31: # October before the 31stis_winter[0] = Falseelse:is_winter[0] = Trueelse: # May-Septemberis_winter[0] = False# 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 Concept
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 Drawing Concepts in Indie:
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:
def __init__(self):# ... (previous code) ...# 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)from statistics import mean, stdevdef 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 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 = f'''Total P&L: {round(total_pnl_real, 2)}%Win Rate: {round(win_rate, 2)}%Trades: {self._total_trades}Profit F: {round(profit_factor, 2)}Sharpe: {round(sharpe_ratio, 2)}Max DD: {round(self._max_drawdown, 2)}%---CURRENT PERIODOpen P&L: {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, winter_start_month, winter_start_day,summer_start_month, summer_start_day, initial_equity):# Now all parameters are customizable!self._winter_start_month = winter_start_monthself._winter_start_day = winter_start_day# ... and so on
@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!
Congratulations! We've journeyed from idea to a fully functional Halloween Strategy indicator. Our indicator doesn't just show signals — it provides comprehensive strategy performance analytics.
@indicator decorator, MainContext class, calc() methodMutSeries for time series, Optional for optional values@plot.background, labels via LabelAbs and LabelRel@param decorators for creating customizable indicatorsindie.math.divideThe Halloween Strategy is a perfect example of how a simple idea can transform into a powerful analytical tool. And the Indie language provides all the necessary tools for creating professional trading indicators.
Remember: even if a strategy worked historically, it doesn't guarantee future results. But now you have a tool to test any seasonal anomalies!
Happy Halloween Trading! 🎃📈


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