Trading Strategy
a simple, widely-taught CPI news strategy — score the inflation release against forecast, buy the dollar on a hot print, sell it on a cool one — returned +$226 with a 77.6% win rate and a profit…
FX Terminal Research · 2026-08-16 · 14 min read
Short answer: a simple, widely-taught CPI news strategy — score the inflation release against forecast, buy the dollar on a hot print, sell it on a cool one — returned +$226 with a 77.6% win rate and a profit factor of 1.21 in our first backtest. After fixing six correctness defects in the test harness, the identical strategy on identical data returned −$194 with an 18.4% win rate and a profit factor of 0.85.
Nothing about the strategy changed. Only the honesty of the measurement did.
This article walks through each defect, quantifies what it was worth, and ends with a checklist you can apply to any backtest — ours, someone else's, or your own spreadsheet. The corrected engine is live and free to use at fxterminal.app/fundamental-strategy.
Deliberately simple, because the point is the measurement, not the idea.
| Parameter | Value |
|---|---|
| Event | US CPI (headline and core, YoY and MoM) |
| Scoring | Each component scored sign(actual − forecast), summed |
| Direction | Positive sum → buy USD; negative → sell USD |
| Instrument | EUR/USD |
| Chart timeframe | H1 |
| Take profit | 100 pips |
| Stop loss | 30 pips |
| Break-even stop | Move to entry after +20 pips |
| Max hold | 72 hours |
| Period | 1 year |
This is close to what a dozen "how to trade the news" guides describe. If it works anywhere, it should work here: CPI is the highest-attention US release, the rule is unambiguous, and the direction is the textbook one.
What was wrong: the default configuration had spreadPips = 0 and commissionPerLot = 0, and the interface never surfaced either field. Every backtest anyone ran was frictionless.
Why it mattered more than it sounds: the corrected strategy's average trade nets +1.43 pips gross. A realistic round-turn cost on EUR/USD is 1.5 pips — and that is the normal spread, not the spread through a CPI print, which routinely widens five to ten times.
So the entire gross edge of the strategy was smaller than a single normal spread. Zeroing costs did not shave a bit off the returns; it was the difference between the strategy having an edge and not having one.
The fix: costs default to 1.5 pips, are editable in a "Trading costs" panel, and a zero-spread setting now raises a visible warning. A separate slippagePips knob exists because filling exactly on the print is the best case, not the expected case.
Rule of thumb: if you cannot state your backtest's cost assumption in pips, your backtest does not have one. Any strategy whose average trade is under about 3 pips gross is a cost-model result, not a strategy result.
What was wrong: running the strategy on EUR/USD, GBP/USD and USD/JPY simultaneously spun up three independent compounding books, each starting at the full account size. Their P&L was then summed and presented as the performance of one account.
Two things break at once:
The fix: a portfolio runner with a shared balance, and risk split across the legs a single release fires. Trading one pair is now a special case of the portfolio path rather than a different code path.
This is the defect I would expect to find most often in retail backtests. Multi-pair testing feels like diversification. When every leg is triggered by the same release, it is leverage.
What was wrong: the backend grouped releases by currency | event_date, then reported the group's earliest timestamp as the entry time.
For CPI alone this is harmless — all components print at 13:30. But a custom strategy scoring both a 13:30 release and a 19:00 release on the same day would enter at 13:30 using the 19:00 print's data. Five and a half hours of foresight, per trade, invisibly.
Lookahead bias is uniquely dangerous because it does not look like a bug. It looks like alpha. The equity curve gets smoother, the win rate rises, and everything about the output says "you found something".
The fix: grouping now keys on the release instant, bucketed to a 15-minute tolerance, so events that genuinely print together group together and events hours apart never do.
This one took two passes.
First problem: the engine filled at candles[entryIdx].close while stamping the entry time as that bar's open. On H1, a 13:30 CPI print therefore entered at the 14:00 close — a full 90 minutes after the release — while the trade log claimed 13:00. Both the price and the timestamp were wrong, and they were wrong in different directions.
First fix: fill at the bar open, and start resolving stops and targets on the entry bar itself.
Second problem, found immediately after: filling at the next H1 open still means a 13:30 print enters at 14:00 — 30 minutes late. Measured against real production data, that fill sat a mean of 4–6 pips (max 22) away from the price quoted on the print, against a 30-pip stop. Worse, the engine never saw what happened in the intervening half hour, so a trade that would have been stopped out at 13:40 and re-entered simply never existed.
Second fix: a release-window endpoint that steps down M1 → M5 → M15 → M30 → H1 and returns the first bar at or after the release instant. Its open is the release-instant price. Resolution then runs in two phases: the fine bars first (which start at the fill, so a release candle's pre-release high and low can never trigger anything — that was another latent lookahead), then the chart candles afterward.
Measured fill accuracy on the corrected engine:
| Backtest span | Exact fills | Worst-case lag |
|---|---|---|
| 1 year | 49 / 49 | 0 |
| 5 years | 156 / 197 | 30 min |
| All available data | 167 / 221 | 30 min |
Precision degrades with age because MT5 keeps a rolling history window per timeframe: M1 reaches back about 3 months, M5 about a year, M15 about 3 years, M30 about 7. The interface states which timeframe answered each fill rather than hiding it.
One counter-intuitive detail worth keeping: 99.4% of releases land on a 5-minute boundary, and a bar that opens exactly on the release instant gives an exact fill regardless of its duration. Coarse resolution is not the same as inaccurate.
What was wrong: the statistics counted any trade with net ≥ 0 as a win. A break-even exit nets exactly $0, so it counted as a win.
The corrected exit mix explains the entire win-rate collapse:
| Exit reason | Share of trades |
|---|---|
| Break-even stop | 53% |
| Stop loss | 29% |
| Time exit (72h cap) | 12% |
| Take profit | 6% |
More than half of all trades ended flat at the break-even stop. Under the old accounting, those 53% were wins — which is how a strategy that reaches its 100-pip target 6% of the time reported a 77.6% win rate.
The fix: three categories, wins / losses / scratches, plus a visible exit-mix breakdown. With a non-zero spread a break-even exit actually nets slightly negative, so it lands in losses — which is correct, and which is why the corrected win rate is 18.4% rather than 25%.
The strategic lesson is bigger than the accounting one. A 20-pip break-even trigger against a 100-pip target means the trade must survive a retracement to entry after moving only a fifth of the way. On a volatile post-release move, it almost never does. The break-even stop was not protecting the strategy; it was the strategy's primary exit. Break-even and trailing stops help trend and breakout systems. On mean-reverting or whipsaw-prone entries — which post-release entries are — they systematically convert winners into scratches.
What was wrong: two things.
Gaps were filled at the exact stop or target level, even when the bar opened straight through it. Real gap fills happen at the open.
And there was no maximum hold. A trade that never reached its stop or target ran indefinitely.
Measured effect of the max-hold cap alone (CPI, 1 year, H1):
| Configuration | Time exits | p90 hold | Max hold | Net |
|---|---|---|---|---|
| Uncapped | 0 | 112 h | 178 h | −$577 |
| Capped at 72 h | 6 | 72 h | 110 h | −$295 |
The fix: gap fills use bar.open, and maxHoldHours (default 72, 0 for unbounded) is a user-facing field with the clock running from the fill instant.
Not part of the original count, but the most instructive.
Stored candle history is not contiguous. EUR/USD H1 holds two stray bars dated 16 March 2022 and then nothing until 6 February 2024 — a 692-day gap.
Releases inside that hole passed the engine's "is this release before my first candle?" guard, found the next available bar, and filled 692 days later as apparently legitimate trades. On the exit side, the mirror version: trades entered at a real 2022 price on M30 data "resolved" 460 days later at the first H1 bar in existence. Twenty-eight of them in a single 5-year EUR/USD run.
These trades looked completely normal in the trade table. Only the entry-to-exit connector lines drawn on the chart made them visible, and even that was initially misread as a rendering bug.
The fix: two guards. The chart timeframe must have a candle within 3 days of the release (clears an FX weekend, rejects a data hole), and resolution stops at the last price before any gap longer than 5 days (clears Christmas and Easter closures). The 5-year run went from 197 trades to 168, with the longest trade dropping from 460 days to under 7.
Generalisable lesson: every backtest silently assumes its price series is continuous. Almost no free price series is. If your harness has no gap guard, some fraction of your trades are fictional, and they will be concentrated in the oldest data where you are least likely to look.
| Metric | Before fixes | After fixes |
|---|---|---|
| Net P&L | +$226 | −$194 |
| Win rate | 77.6% | 18.4% |
| Profit factor | 1.21 | 0.85 |
| Average trade (gross) | — | +1.43 pips |
| Take-profit hit rate | — | 6% |
CPI template, EUR/USD, H1, 1 year, 1.5-pip spread, 30-pip stop, 100-pip target, 20-pip break-even, 72-hour max hold.
Do not quote the headline number without that configuration line. A different pair, target, stop or period gives a different answer. What generalises is not "the CPI trade loses $194" — it is that the strategy's gross edge (1.43 pips per trade) is smaller than its transaction cost, and that no amount of parameter tuning fixes a sub-spread edge.
The direction rule is not obviously wrong — our news impact by pair dataset shows US CPI resolving dollar-negative on EUR/USD in 63% of 60 releases, which is at least suggestive. The failure is in the exit structure. A 100-pip target against a 30-pip stop needs a 3.3:1 move, and CPI's median 30-minute excursion on EUR/USD is 38 pips. The target sits beyond the typical move, the stop sits inside the typical adverse excursion, and the break-even trigger fires before either. The strategy is structurally designed to scratch.
Two things, deliberately, because they are how a negative result gets laundered into a positive one.
We did not tune the parameters until it passed. With a 1-year sample and four adjustable numbers, finding a profitable combination is trivial and meaningless.
We did not switch to a scoring method that happened to work better. The engine gained a second mode during this work — surprise scoring, which divides actual − forecast by that event's mean absolute surprise and clamps to ±3, instead of using sign alone. It is a genuine improvement in expressiveness: on the July 2026 NFP shape (payrolls 57 vs 110 forecast, unemployment 4.2 vs 4.3), sign-only scoring nets to zero and takes no trade, while surprise scoring gives −2.52 + 1.50 = −1.02 and sells the dollar. But it was added because sign-only discards magnitude, not because it rescued the result.
Apply this to ours, to a vendor's, or to your own spreadsheet. Each item corresponds to a defect above.
Is trading CPI releases profitable? Not with the standard textbook structure. In our corrected 1-year EUR/USD H1 test, a sign-scored CPI strategy with a 100-pip target, 30-pip stop and 20-pip break-even returned −$194 with a profit factor of 0.85 and an average gross edge of 1.43 pips — below the round-turn spread. The direction rule shows a mild statistical tilt; the exit structure destroys it.
Why did fixing bugs make the win rate fall from 77% to 18%? Because break-even exits were being counted as wins. Fifty-three per cent of trades ended flat at the break-even stop and only 6% reached the take-profit. Once scratches were separated from wins, the true picture emerged: the strategy almost never reaches its target and almost never takes a full loss — it mostly ends nowhere.
What is lookahead bias in a news backtest? Using information that was not available at the moment of entry. In our case, releases were grouped by calendar date and the group's earliest timestamp was used as the entry time, so a strategy scoring both a 13:30 and a 19:00 release entered at 13:30 with knowledge of the 19:00 print. It inflates results without ever looking like an error.
Do break-even stops improve news trading strategies? Generally no. A break-even stop converts a trade into a scratch as soon as price retraces to entry after a modest favourable move. Post-release price action is whipsaw-prone by nature, so break-even triggers fire constantly. Break-even and trailing stops suit trend and breakout systems, where the favourable excursion is sustained. For fades and event entries they mostly remove the winners.
How accurate are the entry fills in FXTerminal's news backtester? For a 1-year run, all 49 fills were exact — the engine found an M1 or M5 bar opening on the release instant. Over 5 years, 156 of 197 were exact with a worst-case lag of 30 minutes, because finer timeframes have shallower stored history. Every trade records its lag and which timeframe answered it, shown in an "Execution accuracy" card.
Can I test my own event combinations rather than a template? Yes. The news and economic data backtester ships CPI, PCE, PPI, Non-Farm Payrolls and UK employment templates, and lets you build your own by picking any calendar events, assigning per-event weights and polarity, and choosing sign or surprise scoring across seven major pairs.
Why do some events never produce a signal? Because scoring requires both an actual and a forecast, and some series never carry a forecast. The eurozone's "Inflation Rate MoM Flash" was scoreable in 0 of 78 releases and Japan's "Inflation Rate MoM" in 0 of 154 — both were dropped from the templates. Japan's "Inflation Rate YoY" is scoreable only 43% of the time and was kept with that caveat.
Where can I test technical strategies instead of news-based ones? The forex strategy backtester covers indicator and price-action rules on the same historical candle data, with the same cost model and statistics.
The uncomfortable part of this exercise is not that the CPI strategy lost money. It is that the broken version produced a 77% win rate and a 1.21 profit factor — numbers that would pass most people's smell test, sit comfortably in a screenshot, and sell a course.
Every one of the six defects was independently plausible. Nobody sets spread = 0 maliciously; it is the default value of a number field. Nobody intends lookahead; it falls out of a reasonable-looking GROUP BY. The 692-day trades were arithmetically correct given the data they were handed.
That is what makes backtest results so hard to trust from the outside, and why the checklist above is worth more than any single result. When you evaluate a strategy — ours or anyone's — the questions that matter are about the harness, not the rules.
The corrected engine, including the exit mix, execution-accuracy reporting and cost model, is free at fxterminal.app/fundamental-strategy. If you build a configuration that survives all ten checklist items and still shows an edge, that is a genuinely interesting result — and the trade log is exportable so someone else can check it.