Back to Blog
STRATEGY ALGO TRADING NEWS DATA

How to Build a News-Driven
Forex Trading AlgorithmUsing Forex Factory Data

TM
Thomas MüllerData Engineering
·June 10, 2024·10 min read

Economic news events are among the most powerful price drivers in Forex. NFP, FOMC, and CPI releases can move EUR/USD 80–150 pips in seconds. Most retail algorithms ignore this — and pay the price. This guide shows you how to build a news-aware strategy that either avoids the danger window or trades the reaction systematically.

Why News Trading Matters

High-impact economic releases aren't just calendar events — they're volatility injection points. In the 60 seconds around an NFP release, EUR/USD can move more than it does in an entire quiet session. Algorithms that don't account for this get obliterated by slippage, spread expansion, and false signals.

Here are the top 5 events that consistently move Forex markets the most:

EventCurrencyAvg Move (pips)Window
Non-Farm Payrolls (NFP)USD80–150 pips±5 min
FOMC Rate DecisionUSD60–120 pips±10 min
CPI / Inflation DataUSD/EUR/GBP40–90 pips±5 min
ECB Rate DecisionEUR50–100 pips±10 min
BOE Rate DecisionGBP45–95 pips±10 min

The key insight: you don't have to trade news to benefit from this data. Simply knowing when not to trade can significantly improve your strategy's performance by eliminating the worst stop-outs.

The Forex Factory News API Structure

PipData's Forex Factory integration delivers two types of news messages over the same WebSocket stream as your tick data:

1. Scheduled Event (published in advance)

{
  "type": "news_event",
  "id": "ff_20240601_nfp",
  "title": "US Non-Farm Payrolls",
  "currency": "USD",
  "impact": "high",
  "scheduled_time": "2024-06-07T12:30:00Z",
  "forecast": "185K",
  "previous": "175K",
  "actual": null,          // null until published
  "actual_published_at": null
}

2. Result Published (sent when the actual figure is released)

{
  "type": "news_event",
  "id": "ff_20240601_nfp",
  "title": "US Non-Farm Payrolls",
  "currency": "USD",
  "impact": "high",
  "scheduled_time": "2024-06-07T12:30:00Z",
  "forecast": "185K",
  "previous": "175K",
  "actual": "206K",        // ← The surprise
  "actual_published_at": "2024-06-07T12:30:03Z"  // 3s after scheduled
}

You receive the scheduled event well in advance (typically hours to days before), giving you time to prepare. The actual result message arrives within seconds of publication — before most retail platforms even process it.

Strategy 1 — Pre-News Position Guard

The simplest and arguably most valuable use of the news feed: automatically pause your strategy and close/reduce positions in the 5 minutes before a high-impact event. This alone eliminates a huge class of bad outcomes.

import asyncio
from datetime import datetime, timezone, timedelta

class NewsGuard:
    """Automatically pause trading around high-impact news events."""

    def __init__(self, trading_bot, pause_minutes=5, resume_minutes=15):
        self.bot = trading_bot
        self.pause_before = timedelta(minutes=pause_minutes)
        self.resume_after = timedelta(minutes=resume_minutes)
        self.paused_until = None

    def handle_news_event(self, event: dict):
        """Called when a news_event message arrives."""
        if event.get("impact") != "high":
            return  # Only care about high-impact

        if event.get("actual") is not None:
            # Result published — schedule resume
            published_at = datetime.fromisoformat(
                event["actual_published_at"].replace("Z", "+00:00")
            )
            self.paused_until = published_at + self.resume_after
            print(f"📊 Result: {event['title']} = {event['actual']} "
                  f"(forecast: {event['forecast']})")
            print(f"   Trading resumes at {self.paused_until:%H:%M:%S} UTC")
        else:
            # Upcoming event — check if we need to pause now
            sched = datetime.fromisoformat(
                event["scheduled_time"].replace("Z", "+00:00")
            )
            now = datetime.now(timezone.utc)
            time_until = sched - now

            if time_until <= self.pause_before:
                self.paused_until = sched + self.resume_after
                # Close all open positions before the event
                asyncio.create_task(self._pause_trading(event, sched))

    async def _pause_trading(self, event, event_time):
        now = datetime.now(timezone.utc)
        mins = (event_time - now).total_seconds() / 60
        print(f"⏸ PAUSING: {event['title']} in {mins:.1f} min")
        await self.bot.close_all_positions(reason=f"pre_news_{event['id']}")
        await self.bot.pause_new_orders(until=self.paused_until)

    def is_paused(self) -> bool:
        if self.paused_until is None:
            return False
        return datetime.now(timezone.utc) < self.paused_until

Strategy 2 — Reaction Breakout After News

For those who want to actively trade news, here's a systematic surprise-magnitude breakout approach. The logic: if the actual result significantly deviates from the forecast, trade the direction of the surprise with a tight stop and quick target.

from dataclasses import dataclass
from typing import Optional

@dataclass
class TradeSignal:
    direction: str       # "BUY" or "SELL"
    pair: str            # e.g. "EURUSD"
    stop_pips: float     # Stop loss in pips
    target_pips: float   # Take profit in pips
    reason: str

class NewsBreakoutStrategy:
    """Trade the reaction to high-impact news surprises."""

    # Minimum surprise % to trigger a trade
    MIN_SURPRISE_PCT = 5.0

    # Risk parameters
    STOP_PIPS   = 8.0
    TARGET_PIPS = 25.0

    # Map currencies to tradable pairs
    CURRENCY_PAIRS = {
        "USD": ["EURUSD", "GBPUSD", "USDJPY"],
        "EUR": ["EURUSD", "EURGBP", "EURJPY"],
        "GBP": ["GBPUSD", "EURGBP", "GBPJPY"],
        "JPY": ["USDJPY", "EURJPY", "GBPJPY"],
    }

    def evaluate_news(self, event: dict) -> Optional[TradeSignal]:
        """Return a trade signal if the news is a significant surprise."""
        if event.get("actual") is None:
            return None  # No result yet

        surprise_pct = self._calculate_surprise(
            event.get("forecast"), event.get("actual"), event.get("unit", "")
        )
        if surprise_pct is None or abs(surprise_pct) < self.MIN_SURPRISE_PCT:
            return None  # Not significant enough

        currency   = event["currency"]
        bullish_usd = surprise_pct > 0  # Positive surprise = bullish for currency

        # Pick the most liquid pair for this currency
        pairs = self.CURRENCY_PAIRS.get(currency, [])
        if not pairs:
            return None

        pair = pairs[0]  # Most liquid

        # Determine direction based on pair structure
        if pair.startswith(currency):
            direction = "SELL" if bullish_usd else "BUY"
        else:
            direction = "BUY" if bullish_usd else "SELL"

        return TradeSignal(
            direction   = direction,
            pair        = pair,
            stop_pips   = self.STOP_PIPS,
            target_pips = self.TARGET_PIPS,
            reason      = (f"{event['title']}: actual={event['actual']} "
                           f"vs forecast={event['forecast']} "
                           f"({surprise_pct:+.1f}% surprise)")
        )

    def _calculate_surprise(self, forecast, actual, unit) -> Optional[float]:
        """Calculate the percentage deviation of actual from forecast."""
        try:
            def parse(v):
                return float(str(v).replace('K','e3').replace('M','e6')
                               .replace('%','').strip())
            f, a = parse(forecast), parse(actual)
            if abs(f) < 0.001:
                return None
            return (a - f) / abs(f) * 100
        except (TypeError, ValueError):
            return None

Risk Management for News Trading

News trading carries unique risks that standard risk models don't fully capture. Here are the rules we recommend:

  • Maximum 0.5% account risk per news trade — news moves are violent and you can hit multiple events in a day
  • Use stops of 8–12 pips — price often snaps back, so wide stops mean you get stopped in the wrong direction anyway
  • Don't trade if spread exceeds 3× normal — during NFP, EUR/USD spreads can widen to 3–5 pips; skip the trade
  • Set time exits — if the trade hasn't hit target within 30 minutes, close it regardless of PnL
  • Never trade two consecutive high-impact events — the second event often reverses the first reaction
def is_tradeable(pair: str, signal: TradeSignal,
                 current_spread_pips: float) -> bool:
    """Pre-trade checklist for news trades."""

    # 1. Spread check — abort if spread is too wide
    normal_spreads = {"EURUSD": 0.6, "GBPUSD": 0.9, "USDJPY": 0.7}
    max_spread = normal_spreads.get(pair, 1.0) * 3.0

    if current_spread_pips > max_spread:
        print(f"✗ Spread too wide: {current_spread_pips:.1f}pip "
              f"(max {max_spread:.1f}pip) — skipping {pair}")
        return False

    # 2. Time check — don't trade if we just had a news event
    # (handled by NewsGuard.is_paused())

    # 3. Signal quality check
    if signal.stop_pips > 15:
        print(f"✗ Stop too wide: {signal.stop_pips}pip — skipping")
        return False

    return True

Backtesting Your News Strategy

PipData provides historical news data alongside historical tick/OHLC data, so you can backtest news strategies with the same fidelity as live trading. The historical news feed includes all scheduled events, forecasts, and actual results going back 5 years.

import requests

API_KEY = "your_api_key_here"
BASE    = "https://api.pipdata.net/v1"

def get_historical_news(start_date, end_date, impact="high"):
    """Fetch historical Forex Factory events for backtesting."""
    resp = requests.get(
        f"{BASE}/news/history",
        params={
            "start":  start_date,   # "2024-01-01"
            "end":    end_date,     # "2024-06-01"
            "impact": impact,
            "currencies": "USD,EUR,GBP,JPY"
        },
        headers={"X-API-Key": API_KEY}
    )
    return resp.json()["events"]

def backtest_news_strategy(events, ohlc_data, strategy):
    """Simple walk-forward backtest over historical news events."""
    results = []

    for event in events:
        if event["actual"] is None:
            continue  # Skip events without results

        signal = strategy.evaluate_news(event)
        if signal is None:
            continue

        # Get the M1 candle immediately after the news
        event_time = event["actual_published_at"]
        entry_candle = get_candle_at(ohlc_data, signal.pair, event_time)
        if entry_candle is None:
            continue

        # Simulate trade
        entry = entry_candle["open"]
        pip   = 0.0001 if "JPY" not in signal.pair else 0.01

        if signal.direction == "BUY":
            stop   = entry - signal.stop_pips * pip
            target = entry + signal.target_pips * pip
        else:
            stop   = entry + signal.stop_pips * pip
            target = entry - signal.target_pips * pip

        outcome = simulate_trade(
            ohlc_data, signal.pair, event_time,
            entry, stop, target, signal.direction
        )
        results.append(outcome)

    wins = sum(1 for r in results if r["result"] == "win")
    print(f"Backtest: {len(results)} trades | "
          f"Win rate: {wins/len(results)*100:.1f}% | "
          f"Net pips: {sum(r['pips'] for r in results):.1f}")
    return results

Conclusion

News-driven strategies don't have to be complicated. Even a simple pre-news guard — pausing trading 5 minutes before high-impact events and resuming 15 minutes after — can meaningfully improve a strategy's performance by eliminating the worst-case scenarios.

If you want to trade the reactions, the surprise-magnitude breakout approach gives you a systematic, repeatable framework that removes emotion from the equation. Combined with broker-matched data and tight execution, it becomes a serious edge.

The full backtesting framework, including the simulate_trade and get_candle_at functions, is available in the PipData subscriber GitHub repository. Pro subscribers also get access to 5 years of historical news data with actual results.
Start Trading with Real News Data

Get real-time Forex Factory integration plus broker-matched tick data. Free 7-day trial included.

Get API Key