Quant500 Academy

3. Your first real trading signal: the MACD crossover

Level 3: Market Data and APIs

So far you have downloaded and visualized prices. In this module you take the step that turns a chart into a strategy: automatically generating buy and sell signals from a technical indicator.

The indicator you are going to use is the MACD (Moving Average Convergence/Divergence), one of the most widely used in technical analysis because it combines trend and momentum in a single number. It is built from two exponential moving averages (EMA) of different lengths: one fast and one slow.

fast_ema = data["Close"].ewm(span=12, adjust=False).mean()
slow_ema = data["Close"].ewm(span=26, adjust=False).mean()

macd = fast_ema - slow_ema
signal = macd.ewm(span=9, adjust=False).mean()

An exponential moving average (EMA) gives more weight to recent prices than to older ones, unlike the simple moving average from the previous module, which weights every day in the window equally. That makes it react faster to recent price changes. The MACD is, quite literally, the difference between the fast EMA (12 periods) and the slow EMA (26 periods); the signal line is, in turn, a 9-period EMA computed on the MACD itself.

The classic interpretation is: when the MACD crosses above the signal line, that is a buy signal; when it crosses below, that is a sell signal. Turning that idea into code means walking through the series day by day and comparing the two lines at each point:

def macd_signals(df):
    buy_price = []
    sell_price = []
    position_open = False

    for i in range(len(df)):
        if df["MACD"].iloc[i] > df["Signal"].iloc[i]:
            if not position_open:
                buy_price.append(df["Open"].iloc[i + 1] if i + 1 < len(df) else float("nan"))   # the signal is born at the close of i; it is executed at the open of i+1
                sell_price.append(float("nan"))
                position_open = True
            else:
                buy_price.append(float("nan"))
                sell_price.append(float("nan"))
        elif df["MACD"].iloc[i] < df["Signal"].iloc[i]:
            if position_open:
                sell_price.append(df["Open"].iloc[i + 1] if i + 1 < len(df) else float("nan"))   # you exit the way you enter: at the next day's open
                buy_price.append(float("nan"))
                position_open = False
            else:
                buy_price.append(float("nan"))
                sell_price.append(float("nan"))
        else:
            buy_price.append(float("nan"))
            sell_price.append(float("nan"))

    return buy_price, sell_price

The important detail in this function is the position_open variable: without it, the code would generate a buy signal on every single day the MACD sits above the signal line, not only at the exact moment of the crossover — which would produce dozens of consecutive buy signals instead of one. position_open acts as memory: it only buys if you did not already have an open position, and it only sells if you did. This pattern — a state variable that remembers what situation the strategy is in — is the basis of practically any signal generator, however sophisticated the indicator in question.

With the two resulting lists, you can mark them directly on the price to see, visually, whether the logic makes sense before trusting it:

data["Buy"], data["Sell"] = macd_signals(data)

plt.scatter(data.index, data["Buy"], color="green", marker="^", label="Buy")
plt.scatter(data.index, data["Sell"], color="red", marker="v", label="Sell")
plt.plot(data["Close"], alpha=0.35, label="Price")
plt.legend()
plt.show()

This example has an important limitation that is worth being clear about right away: it generates signals, not a complete strategy. It does not compute how much capital to risk, it has no stop-loss, and it has not been through any rigorous backtest — that comes in later levels. For now, the goal is to understand the pattern: indicator, comparison, signal, state.

To go deeper into this lesson

The rest of Quant500

What you are learning, applied to the index every day:

The model portfolio, weekly in your inbox

Every Saturday, its week in one minute: how it went, the best and the worst, and which insiders bought with their own money. Plus, the day the model is rebuilt, which stocks come in and which go out. A simulation, not advice. At most one email a week, no ads, no account; one-click unsubscribe. Made by one person: Pol Gracia.

A model portfolio with simulated results, not a personalized recommendation. Your email is used for this only: privacy.

Behind the Academy and the rest of Quant500 there is one person: Pol Gracia Serrahima.