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:
| Event | Currency | Avg Move (pips) | Window |
|---|---|---|---|
| Non-Farm Payrolls (NFP) | USD | 80–150 pips | ±5 min |
| FOMC Rate Decision | USD | 60–120 pips | ±10 min |
| CPI / Inflation Data | USD/EUR/GBP | 40–90 pips | ±5 min |
| ECB Rate Decision | EUR | 50–100 pips | ±10 min |
| BOE Rate Decision | GBP | 45–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 thesimulate_tradeandget_candle_atfunctions, is available in the PipData subscriber GitHub repository. Pro subscribers also get access to 5 years of historical news data with actual results.
Get real-time Forex Factory integration plus broker-matched tick data. Free 7-day trial included.