{"spec_id":"indicator-ema","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nindicator-ema: Exponential Moving Average (EMA) Indicator Chart\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-19\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Data\nnp.random.seed(42)\nn_days = 120\ndates = pd.date_range(start=\"2024-01-01\", periods=n_days, freq=\"B\")\nreturns = np.random.normal(0.0005, 0.02, n_days)\nprice = 100 * np.cumprod(1 + returns)\n\ndf = pd.DataFrame({\"date\": dates, \"close\": price})\ndf[\"ema_12\"] = df[\"close\"].ewm(span=12, adjust=False).mean()\ndf[\"ema_26\"] = df[\"close\"].ewm(span=26, adjust=False).mean()\n\n# Detect EMA crossover points (sign change in ema_12 - ema_26)\ndiff = df[\"ema_12\"] - df[\"ema_26\"]\ncrossover_mask = (diff.shift(1) * diff < 0) & diff.shift(1).notna()\ncrossover_df = df[crossover_mask][[\"date\", \"ema_12\"]].rename(columns={\"ema_12\": \"value\"})\n\n# Melt into long format for multi-series chart\ndf_price = df[[\"date\", \"close\"]].rename(columns={\"close\": \"value\"})\ndf_price[\"series\"] = \"Close Price\"\ndf_ema12 = df[[\"date\", \"ema_12\"]].rename(columns={\"ema_12\": \"value\"})\ndf_ema12[\"series\"] = \"EMA 12\"\ndf_ema26 = df[[\"date\", \"ema_26\"]].rename(columns={\"ema_26\": \"value\"})\ndf_ema26[\"series\"] = \"EMA 26\"\ndf_long = pd.concat([df_price, df_ema12, df_ema26], ignore_index=True)\n\n# Scales\ncolor_scale = alt.Scale(domain=[\"Close Price\", \"EMA 12\", \"EMA 26\"], range=IMPRINT)\nstroke_scale = alt.Scale(domain=[\"Close Price\", \"EMA 12\", \"EMA 26\"], range=[3.5, 2.5, 2.5])\n\n# Line chart layer\nlines = (\n    alt.Chart(df_long)\n    .mark_line()\n    .encode(\n        x=alt.X(\"date:T\", title=\"Date\", axis=alt.Axis(format=\"%b %Y\", labelAngle=-45)),\n        y=alt.Y(\"value:Q\", title=\"Price (USD)\", scale=alt.Scale(zero=False)),\n        color=alt.Color(\"series:N\", scale=color_scale, legend=alt.Legend(title=\"Series\", orient=\"top-right\")),\n        strokeWidth=alt.StrokeWidth(\"series:N\", scale=stroke_scale, legend=None),\n        tooltip=[\n            alt.Tooltip(\"date:T\", title=\"Date\", format=\"%Y-%m-%d\"),\n            alt.Tooltip(\"series:N\", title=\"Series\"),\n            alt.Tooltip(\"value:Q\", title=\"Price\", format=\"$.2f\"),\n        ],\n    )\n)\n\n# Crossover markers (diamond points where EMA 12 crosses EMA 26)\ncrossover_markers = (\n    alt.Chart(crossover_df)\n    .mark_point(size=300, shape=\"diamond\", filled=True, color=\"#BD8233\", opacity=0.85)\n    .encode(\n        x=alt.X(\"date:T\"), y=alt.Y(\"value:Q\"), tooltip=[alt.Tooltip(\"date:T\", title=\"EMA Crossover\", format=\"%Y-%m-%d\")]\n    )\n)\n\n# Compose and style\nchart = (\n    alt.layer(lines, crossover_markers)\n    .properties(width=1600, height=900, title=\"indicator-ema · python · altair · anyplot.ai\", background=PAGE_BG)\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.10,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=18,\n        titleFontSize=22,\n        tickSize=10,\n    )\n    .configure_title(fontSize=28, color=INK, anchor=\"middle\")\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        titleFontSize=20,\n        labelFontSize=18,\n        symbolStrokeWidth=4,\n        symbolSize=200,\n    )\n)\n\n# Save\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}