Building a Halloween Strategy Indicator in Indie 🎃

From Idea to ImplementationIntroduction: When Pumpkins Predict Profits

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. 

Step 1: Understanding the Halloween Strategy Idea

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:

  1. Visual Period Separation: We want to immediately see when we're "in the market" (winter period) and when we're resting (summer period). I imagine this as colored zones on the chart — orange for the "pumpkin" season and navy blue for summer vacation.
  2. Entry and Exit Signals: Clear "Buy" and "Sell" markers at season transitions.
  3. Performance Statistics: A panel with key metrics — how much we earned, winning trade percentage, maximum drawdown. After all, we want to know if our pumpkin magic is working!
  4. Date Flexibility: What if we want to test different dates? For example, entering on November 1st instead of October 31st? We need customizable parameters.

Step 2: Creating the Foundation and Adding Colored Periods

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:

  • @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).

Now let's add logic to determine the current period:

def calc(self):    # The calc() method is called for each bar on the chart    timestamp = self.time[0]  # Get the current bar's timestamp    dt = datetime.utcfromtimestamp(timestamp)        month = dt.month    day = dt.day        # MutSeries - mutable data series, one of Indie's key structures    is_winter = MutSeries[bool].new(False)        # Logic for determining winter period    # Winter: from October 31 to May 1    if month < 5:  # January-April        is_winter[0] = True    elif month == 5 and day < 1:  # May before the 1st        is_winter[0] = True    elif month >= 10:  # October-December        if month == 10 and day < 31:  # October before the 31st            is_winter[0] = False        else:            is_winter[0] = True    else:  # May-September        is_winter[0] = False        # Return the appropriate background color    if 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.

Step 3: Adding Buy/Sell Markers

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 periods    is_winter_start = is_winter[0] and not is_winter[1]  # Winter start    is_winter_end = not is_winter[0] and is_winter[1]    # Winter end        if is_winter_start:        # Draw buy marker        self.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 marker        self.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:

  • 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 labels. This small but important detail improves chart readability!

Step 4: Planning Statistics — What Do We Want to Know?

Before calculating statistics, let's think like experienced traders: which metrics are truly important for evaluating a strategy?

My Wishlist

  1. Total P&L (%) — Overall profit/loss. The main question: how much did we earn?
  2. Win Rate (%) — Percentage of profitable trades. How often does our pumpkin magic work?
  3. Number of Trades — For understanding statistical significance.
  4. Profit Factor — Ratio of total profit to total loss. Shows how many times profit exceeds losses.
  5. Sharpe Ratio — Risk-adjusted return ratio. Higher is better for risk/return balance.
  6. Maximum Drawdown — Maximum decline from peak. How painful can the worst moments be?
  7. Open P&L — Current open position. What's happening right now?

How Will We Calculate?

  • P&L: Track entry and exit prices, calculate percentage change
  • Win Rate: Keep counters for winning and total trades
  • Profit Factor: Separately sum profits and losses
  • Sharpe: Save all trade P&Ls, then calculate mean and standard deviation
  • Drawdown: Track peak equity value and current value

Step 5: Implementing Statistics Calculations

Now let's turn our plan into code. Add necessary variables to the constructor:

def __init__(self):    # ... (previous code) ...        # Trading variables    self._position = 0    self._open_price: Optional[float] = None    self._total_trades = 0    self._win_trades = 0        # For Profit Factor calculation    self._total_profit = 0.0    self._total_loss = 0.0        # For Sharpe Ratio calculation    self._trades_pnl_list: list[float] = []        # For Maximum Drawdown calculation    self._initial_equity = 100.0    self._current_equity = 100.0    self._peak_equity = 100.0    self._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 price            self._position += 1        if is_winter_end:        if self._open_price is not None:            # Calculate current trade P&L            trade_pnl_percent = (divide(self.close[0], self._open_price.value()) - 1.0) * 100.0                        # Save for Sharpe calculation            self._trades_pnl_list.append(trade_pnl_percent)                        # Update equity with compounding            self._current_equity = self._current_equity * (1.0 + trade_pnl_percent / 100.0)                        # Update Maximum Drawdown            if self._current_equity > self._peak_equity:                self._peak_equity = self._current_equity                        current_dd = divide(self._peak_equity - self._current_equity, self._peak_equity) * 100.0            if current_dd > self._max_drawdown:                self._max_drawdown = current_dd                        # Separate profitable and losing trades            if trade_pnl_percent > 0.0:                self._win_trades += 1                self._total_profit += trade_pnl_percent            elif trade_pnl_percent < 0.0:                self._total_loss += abs(trade_pnl_percent)                        self._total_trades += 1            self._open_price = None            self._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.

Step 6: Displaying Statistics on the Chart

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 corner    self._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 metrics    open_pnl = 0.0    if self._position > 0 and self._open_price is not None:        open_pnl = (divide(self.close[0], self._open_price.value()) - 1.0) * 100.0        win_rate = divide(self._win_trades, self._total_trades, 0) * 100.0    total_pnl_real = self._current_equity - self._initial_equity    profit_factor = divide(self._total_profit, self._total_loss, 0)        # Sharpe Ratio    sharpe_ratio = 0.0    if 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 text    stats_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_text    self.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!

Bonus: Adding Flexibility Through Parameters

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_month        self._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!

Conclusion: What We Built and What We Learned

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.

Key Indie Concepts We've Mastered:

  1. Indicator Structure@indicator decorator, MainContext class, calc() method
  2. Data HandlingMutSeries for time series, Optional for optional values
  3. Visualization: Background areas via @plot.background, labels via LabelAbs and LabelRel
  4. Parameterization@param decorators for creating customizable indicators
  5. Mathematics: Safe operations through indie.math.divide
  6. Positioning: Absolute vs relative element placement

The 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! 🎃📈


Comments
Not authorized user image
No Comments yet image

Be the first to comment

Publish your first comment to unleash the wisdom of crowd.