Simple Moving Average (SMA) Indicator Chart — plotnine

A Simple Moving Average (SMA) chart displays price or value data with one or more SMA overlays, typically showing short, medium, and long-term periods (e.g., 20, 50, 200-day). Each SMA line smooths out price fluctuations by averaging the last N data points, revealing underlying trends at different time scales. This multi-period overlay is fundamental to technical analysis, helping traders identify trend direction, support/resistance levels, and potential crossover signals.

Simple Moving Average (SMA) Indicator Chart rendered with plotnine

Python source (plotnine)

""" anyplot.ai
indicator-sma: Simple Moving Average (SMA) Indicator Chart
Library: plotnine 0.15.4 | Python 3.13.13
Quality: 88/100 | Updated: 2026-05-19
"""

import os

import numpy as np
import pandas as pd
from plotnine import (
    aes,
    annotate,
    element_line,
    element_rect,
    element_text,
    geom_line,
    ggplot,
    labs,
    scale_color_manual,
    scale_x_datetime,
    theme,
    theme_minimal,
)


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233"]
GOLDEN_COLOR = "#DDCC77"  # imprint amber — "golden cross" semantic
DEATH_COLOR = INK_SOFT  # muted gray — "death" signal, theme-adaptive

# Data — bull market (days 1–150) followed by bear market (days 150–300),
# engineered to produce a visible death cross once SMA 200 is available
np.random.seed(42)
n_days = 300
dates = pd.date_range("2024-01-01", periods=n_days, freq="B")

base_price = 150
returns = np.random.normal(0.0, 0.012, n_days)
trend = np.zeros(n_days)
trend[:150] = 0.002  # moderate uptrend
trend[150:] = -0.003  # sharper reversal
returns = returns + trend
close = base_price * np.cumprod(1 + returns)

# Calculate SMAs
df = pd.DataFrame({"date": dates, "close": close})
df["sma_20"] = df["close"].rolling(window=20).mean()
df["sma_50"] = df["close"].rolling(window=50).mean()
df["sma_200"] = df["close"].rolling(window=200).mean()

# Detect SMA 50 / SMA 200 crossovers (golden cross & death cross)
valid = df.dropna(subset=["sma_50", "sma_200"]).copy()
valid["diff"] = valid["sma_50"] - valid["sma_200"]
valid["prev_diff"] = valid["diff"].shift(1)
crossovers = valid[valid["prev_diff"].notna() & (valid["prev_diff"] * valid["diff"] < 0)].copy()
crossovers["cross_type"] = crossovers["diff"].apply(lambda d: "Golden Cross" if d > 0 else "Death Cross")

# Reshape to long format for plotnine
df_long = pd.melt(
    df, id_vars=["date"], value_vars=["close", "sma_20", "sma_50", "sma_200"], var_name="series", value_name="price"
)
series_labels = {"close": "Price", "sma_20": "SMA 20", "sma_50": "SMA 50", "sma_200": "SMA 200"}
df_long["series"] = df_long["series"].map(series_labels)
series_order = ["Price", "SMA 20", "SMA 50", "SMA 200"]
df_long["series"] = pd.Categorical(df_long["series"], categories=series_order, ordered=True)

colors = {"Price": IMPRINT[0], "SMA 20": IMPRINT[1], "SMA 50": IMPRINT[2], "SMA 200": IMPRINT[3]}

# Separate data to draw SMAs behind price line at different thicknesses
price_data = df_long[df_long["series"] == "Price"]
sma_data = df_long[df_long["series"] != "Price"]

plot = (
    ggplot(df_long, aes(x="date", y="price", color="series"))
    + geom_line(data=sma_data, size=1.2, alpha=0.85)  # SMAs drawn first (behind price)
    + geom_line(data=price_data, size=2.5, alpha=0.95)  # Price drawn on top, prominently
    + scale_color_manual(values=colors)
    + scale_x_datetime(date_breaks="2 months", date_labels="%b %Y")
    + labs(x="Date", y="Price ($)", title="indicator-sma · python · plotnine · anyplot.ai", color="")
    + theme_minimal()
    + theme(
        figure_size=(16, 9),
        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_background=element_rect(fill=PAGE_BG),
        plot_title=element_text(size=24, weight="bold", color=INK),
        axis_title=element_text(size=20, color=INK),
        axis_text=element_text(size=16, color=INK_SOFT),
        axis_text_x=element_text(size=16, rotation=30, ha="right", color=INK_SOFT),
        legend_text=element_text(size=16, color=INK_SOFT),
        legend_position=(0.02, 0.98),
        legend_direction="vertical",
        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
        panel_grid_major_y=element_line(color=INK, size=0.3, alpha=0.10),
        panel_grid_major_x=element_line(alpha=0),
        panel_grid_minor=element_line(alpha=0),
    )
)

# Annotate crossover signals with vertical dashed lines
for _, row in crossovers.iterrows():
    is_golden = row["cross_type"] == "Golden Cross"
    color = GOLDEN_COLOR if is_golden else DEATH_COLOR
    plot = plot + annotate("vline", xintercept=row["date"], color=color, size=0.9, alpha=0.7, linetype="dashed")

plot.save(f"plot-{THEME}.png", dpi=300)

Part of Simple Moving Average (SMA) Indicator Chart on anyplot.ai.

Other implementations