• Uncategorised
  • 0

Implementing EMA Crossover in Java — A Practical Guide

Implementing EMA Crossover in Java — A Practical Guide

Implementing EMA Crossover in Java

A hands-on guide to building a fast/slow EMA bullish signal — math, code, and a runnable example.

The EMA crossover is one of the simplest momentum filters in trading: when the fast EMA (e.g. 9-period) sits above the slow EMA (e.g. 21-period), short-term trend is stronger than the medium-term trend — a classic bullish setup.

This post walks through the math, then shows a clean Java implementation you can drop into a scanner, backtester, or algo engine.


1. What EMA Actually Computes

An Exponential Moving Average (EMA) is a weighted average where recent closes matter more than older ones. For period N:

α (multiplier) = 2 / (N + 1)

EMA_today = α × close_today + (1 − α) × EMA_yesterday

Equivalent update form (what we use in code):

EMA = (close − EMA) × α + EMA
Periodα1 − αMeaning
EMA90.200.8020% new price, 80% old average — reacts fast
EMA21≈ 0.09≈ 0.919% new price — smooth, lags more

Weight on a bar k steps ago: α × (1−α)^k. For EMA9, the last ~9 bars contribute ~87% of the value.

Key insight: “Fast” and “slow” refer to responsiveness, not line height. In an uptrend, fast EMA > slow EMA. In a downtrend, fast EMA < slow EMA. Same candles, different α — that’s how crossovers happen.

2. Data Model

We only need the close price from each candle. A minimal record:

public record Candle(
    Instant timestamp,
    double open,
    double high,
    double low,
    double close,
    long volume
) {}

3. Core EMA Function in Java

This is the heart of the algo. It seeds from the first close in the window, then walks forward applying the recurrence.

/**
 * Computes EMA over a list of candles using close prices.
 *
 * @param candles ordered oldest → newest
 * @param period  smoothing period (e.g. 9 or 21)
 * @return final EMA value after processing all candles
 */
private static double ema(List<Candle> candles, int period) {
  if (candles.isEmpty()) {
    return 0;
  }
  double multiplier = 2.0 / (period + 1);
  double value = candles.get(0).close();  // seed

  for (int i = 1; i < candles.size(); i++) {
    double close = candles.get(i).close();
    value = (close - value) * multiplier + value;
  }
  return value;
}

Line-by-line

  • multiplier = 2 / (period + 1) — standard EMA α for period N
  • value = candles.get(0).close() — initial seed (first bar in window)
  • Loop applies (close - value) * α + value for each subsequent bar

Numeric trace (EMA9, two bars)

Seed:  close[0] = 100  →  EMA = 100
Bar 1: close[1] = 110  →  EMA = (110 − 100) × 0.20 + 100 = 102

4. The Bullish Crossover Algo

Signal logic:

  • BUY (bullish): fastEma > slowEma
  • NO BUY: fastEma ≤ slowEma

Both EMAs are computed on the same window — the last slowPeriod candles (default 21). The period only changes α, not how many bars we iterate.

public final class EmaBullishAlgo {

  private static final int DEFAULT_FAST = 9;
  private static final int DEFAULT_SLOW = 21;

  public static EmaResult evaluate(List<Candle> candles,
                                      int fastPeriod,
                                      int slowPeriod) {

    if (candles.size() < slowPeriod + 2) {
      return EmaResult.insufficientData();
    }

    // Same window for both EMAs — last `slowPeriod` bars
    List<Candle> window =
        candles.subList(candles.size() - slowPeriod, candles.size());

    double fastEma = ema(window, fastPeriod);
    double slowEma = ema(window, slowPeriod);

    boolean bullish = fastEma > slowEma;
    double gapPct = slowEma > 0
        ? ((fastEma - slowEma) / slowEma) * 100.0
        : 0;

    return new EmaResult(bullish, fastEma, slowEma, gapPct);
  }

  private static double ema(List<Candle> candles, int period) {
    if (candles.isEmpty()) return 0;
    double multiplier = 2.0 / (period + 1);
    double value = candles.get(0).close();
    for (int i = 1; i < candles.size(); i++) {
      value = (candles.get(i).close() - value) * multiplier + value;
    }
    return value;
  }
}

Result record

public record EmaResult(
    boolean bullish,
    double fastEma,
    double slowEma,
    double gapPct
) {
  static EmaResult insufficientData() {
    return new EmaResult(false, 0, 0, 0);
  }
}

5. Runnable Example

Copy this into a single file to see the algo in action:

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;

public class EmaCrossoverDemo {

  public static void main(String[] args) {
    List<Candle> candles = sampleUptrend();

    EmaResult result = EmaBullishAlgo.evaluate(candles, 9, 21);

    System.out.printf("Fast EMA (9):  %.2f%n", result.fastEma());
    System.out.printf("Slow EMA (21): %.2f%n", result.slowEma());
    System.out.printf("Gap:           %.2f%%%n", result.gapPct());
    System.out.printf("Signal:        %s%n",
        result.bullish() ? "BULLISH ✓" : "NOT BULLISH");
  }

  static List<Candle> sampleUptrend() {
    List<Candle> list = new ArrayList<>();
    double[] closes = {
        100, 101, 102, 101, 103, 104, 105, 104, 106,
        107, 108, 107, 109, 110, 111, 112, 113, 114,
        115, 116, 117, 118, 119, 120
    };
    for (double c : closes) {
      list.add(new Candle(Instant.now(), c, c, c, c, 1000));
    }
    return list;
  }
}
Expected on uptrend: fastEma > slowEma → bullish = true. Flip the closes into a downtrend and fastEma will drop below slowEma.

6. Optional: Confidence Score

When the signal passes, you can rank strength by how far fast EMA sits above slow EMA:

double gapPct = ((fastEma - slowEma) / slowEma) * 100.0;
double score = Math.min(100, Math.max(0, 55 + Math.min(45, gapPct * 15)));

Base score 55 when barely above; caps at 100 for wide separation. Useful when multiple symbols pass the filter and you need a ranking.


7. Production Notes

Minimum candle count

Require at least slowPeriod + 2 candles so the window is meaningful and you have buffer history.

Seeding vs TradingView

This implementation seeds EMA from the first close in the window. TradingView often seeds with an SMA of the first N bars. Values may differ slightly, but crossover direction (bullish vs bearish) is usually consistent.

Longer history = smoother EMA

For closer parity with charting tools, compute EMA over 50–100+ bars, not just the last 21. Period (9/21) still sets α; extra history improves accuracy.

Plug into an algo engine

Wrap in an interface so scanners can compose multiple signals:

public interface Algo {
  String id();
  AlgoOutcome evaluate(AlgoContext ctx);
}

EmaBullishAlgo reads candles + params from context, returns pass/fail with fastEma, slowEma, and score in the outcome metadata.


8. Cheat Sheet

ConceptJava / Formula
α for period N2.0 / (N + 1)
EMA update(close - ema) * alpha + ema
Blend formalpha * close + (1 - alpha) * ema
Weight k bars agoalpha * (1 - alpha)^k
Bullish signalfastEma > slowEma
WindowLast slowPeriod candles (both EMAs)

EMA crossover is not a magic bullet — it lags price and whipsaws in sideways markets. Use it as one filter among volume, VWAP, or breakout confirmation in a rule pool.

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *