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_priceThe 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.