Tutorial/FX forecasting
Tutorial

Can you beat a random walk at FX forecasting? Run the test in 30 lines

The classic result says a "no change" forecast beats every model out of sample. Here is the code to put that to the test yourself, against live daily rates.

MMexchangerate.dev·Jul 6, 2026·7 min read

The bar any exchange-rate forecast has to clear is the random walk: the do-nothing forecast that tomorrow's rate equals today's. Since Meese and Rogoff (1983) almost nothing beats it at short horizons, and a 2025 Fed working paper reconfirmed it for modern machine learning. This is the hands-on companion to the forty-year wall: about 30 lines of Python that pull the data, build the benchmark, and score a model honestly.

Key points
The benchmark every FX forecast must beat is the random walk: tomorrow's rate equals today's. On daily major pairs it is remarkably hard to beat.
You can run the whole test on the free tier: pull the series, build the no-change benchmark, and score with out-of-sample R² plus a Clark-West test.
Drop forward-filled weekend and holiday rows (the is_forward_filled flag) or your model quietly learns from flat Sundays.
Adding model complexity makes out-of-sample accuracy worse, not better. You watch overfitting happen in real time.
The companion post runs this on five currency pairs and shows how easily a couple of defensible choices can manufacture a "significant" edge.

The benchmark: "tomorrow equals today"

In 1983, Meese and Rogoff showed that no macro model could out-forecast a model that says tomorrow's exchange rate will be whatever it is today. Interest-rate differentials, money supply, purchasing-power parity: none of them helped. That "model" is the random walk, and forty years of increasingly clever economics has mostly bounced off it. We cover why in Can you predict exchange rates? This guide is the practical half: the code to check the claim yourself.

In September 2025 a Fed economist put modern machine learning through the same test. The paper, *Virtue or Mirage? Complexity in Exchange Rate Prediction* (Rehim Kılıç, FEDS 2025-089), pitted Ridge regression on Random Fourier Features against the random walk, judged by out-of-sample R² and the Clark-West and Diebold-Mariano tests. The verdict was careful: complexity gave "only modest, localized gains" that were "fragile and rarely statistically significant," and never translated into a systematic edge over the random walk.

What we can and cannot reproduce
The paper uses monthly macro fundamentals a rates API does not serve, so we cannot replicate it exactly. But we can test the load-bearing question, the one most builders actually hit: using only price history, can a trained model beat "tomorrow equals today"?

Pull the data

Five years of daily EUR/USD from the /v1/range endpoint. The free tier needs no key to start.

python — fetch a daily seriescopy
import requests, numpy as np, pandas as pd

def fetch_range(base, symbol, start, end, key=None):
    rows, cursor = [], None
    headers = {"Authorization": f"Bearer {key}"} if key else {}
    while True:
        p = {"base": base, "symbols": symbol, "start_date": start, "end_date": end}
        if cursor: p["cursor"] = cursor
        d = requests.get("https://api.exchangerate.dev/v1/range",
                         params=p, headers=headers, timeout=30).json()
        rows += [{"date": r["date"], "rate": r["rates"][symbol]} for r in d["data"]]
        if not d.get("has_more"): break
        cursor = d["next_cursor"]
    df = pd.DataFrame(rows).drop_duplicates("date")
    df["date"] = pd.to_datetime(df["date"])
    return df.sort_values("date").set_index("date")

fx = fetch_range("EUR", "USD", "2021-01-01", "2026-01-01")

One thing worth doing right: each row carries an is_forward_filled flag and a source field. Weekends and holidays are forward-filled, so a serious study drops those rows. You do not want your model "learning" from a flat Sunday, a small version of the look-ahead traps covered in Backfill without look-ahead bias.

Build the benchmark and a challenger

The random walk predicts a zero return every day (the driftless martingale Meese and Rogoff used). Give the challenger every cheap edge from the price series alone, momentum, mean reversion, realized volatility, and let Ridge (the paper's own estimator) predict tomorrow's return.

python — features and a Ridge modelcopy
from sklearn.linear_model import Ridge

fx["ret"] = np.log(fx["rate"]).diff()
f = pd.DataFrame(index=fx.index)
for lag in (1, 2, 3, 5, 10): f[f"lag{lag}"] = fx["ret"].shift(lag)
f["mom5"], f["mom20"] = fx["ret"].rolling(5).sum().shift(1), fx["ret"].rolling(20).sum().shift(1)
f["vol10"], f["y"] = fx["ret"].rolling(10).std().shift(1), fx["ret"]
f = f.dropna()

split = int(len(f) * 0.7)
X = [c for c in f.columns if c != "y"]
m = Ridge(alpha=1.0).fit(f.iloc[:split][X], f.iloc[:split]["y"])
pred, y = m.predict(f.iloc[split:][X]), f.iloc[split:]["y"].values

Score it honestly

Out-of-sample R² against the random walk (positive means you won), plus the Clark-West test, the right one when the random walk is nested inside your model.

python — out-of-sample R² and Clark-Westcopy
from scipy import stats
r2 = 1 - np.sum((y - pred)**2) / np.sum(y**2)
f_hat = y**2 - ((y - pred)**2 - pred**2)
t = f_hat.mean() / (f_hat.std(ddof=1) / np.sqrt(len(f_hat)))
print(f"OOS R2 {r2:+.4%} | Clark-West t {t:+.2f} p {1-stats.norm.cdf(t):.3f}")

Run it across EUR/USD, GBP/USD, USD/JPY and a few windows. The pattern is stubborn. Out-of-sample R² sits at or below zero, from roughly flat to a couple of percent negative, meaning the trained model usually did worse than assuming no change at all. The Clark-West stat rarely clears significance, and the cells that flirt with it move around when you nudge the split or the standard errors. Crank up the feature count to add "complexity" and out-of-sample only gets worse. You are watching overfitting happen in real time. We ran the full test on five pairs and published every number.

Why this matters if you are building

If you ship a "predicted rate" feature, the baseline you have to beat is return today's rate, and on daily major-pair data that baseline is remarkably hard to beat. A forecast endpoint that quietly does worse is a confidently wrong number sitting in someone's checkout flow or treasury dashboard.

The defensible value is elsewhere. Serve the level accurately and fast, since the best point forecast for the next rate really is the current one. Quantify uncertainty instead of point-forecasting: volatility and confidence bands are genuinely useful and do not require beating the random walk. And be honest about horizon, because predictability is roughly nil day-to-day and only creeps up over months, which is where the literature finds what little edge exists.

That flat random-walk line is information. It is the market saying today's price already holds most of what is knowable about tomorrow's. For a data provider, the winning move is to publish that price more accurately and faster than anyone, and to be straight about the limits of prediction.

MM
exchangerate.dev
FX data guides for developers building with indicative rates.

Keep reading

BlogWe tried to beat the random walk on five pairsRead GuideCan you predict exchange rates? The forty-year wallRead GuideBackfill FX rates without look-ahead biasRead