Why a morning-prep agent is the highest-ROI bot you can build first
If you trade FX discretionarily or run a semi-automated book, the most expensive 45 minutes of your day are the ones before London open. You are pulling the release calendar, cross-checking which prints are tier-one, looking at consensus, scanning overnight moves on USD/JPY, EUR/USD, GBP/USD, and trying to remember which central bank speaker matters today. It is repetitive, error-prone, and forces you to rebuild context every morning from scratch.
This is exactly the kind of task that an AI agent does better than a human: bounded scope, structured inputs, deterministic output. In this guide you will build a real-time FX event agent that runs before your trading day, pulls live event data from FXMacroData, ranks today's releases by likely market impact, and delivers a structured briefing you can read in 90 seconds.
By the end you will have a script you can schedule on any machine — laptop, Raspberry Pi, cloud VM — that turns the morning prep from a chore into a checklist.
A Python agent that runs at 06:30 UTC daily, queries the FXMacroData release calendar for the next 24 hours across the G10, ranks events by impact, summarises them through an LLM, and pushes a structured briefing to Telegram or Slack.
Prerequisites
- Python 3.10+ and pip.
- An FXMacroData API key from API Management.
- An LLM endpoint you control. Any of the following works:
- Anthropic Claude (recommended for reasoning quality).
- OpenAI GPT-4-class.
- Local Hermes/Llama via Ollama for zero-cost runs.
- A Telegram bot token (via
@BotFather) or a Slack incoming webhook URL — either works as the delivery channel. - A scheduler.
cronon Linux/macOS, Task Scheduler on Windows, or any cloud cron will do.
Install dependencies:
pip install requests python-dotenv
Create a .env file with your secrets:
FXMD_API_KEY=your_fxmacrodata_key
LLM_PROVIDER=anthropic # or "openai" or "ollama"
ANTHROPIC_API_KEY=sk-ant-...
TELEGRAM_BOT_TOKEN=...
TELEGRAM_CHAT_ID=...
Step 1: Pull the next 24 hours from the FXMacroData release calendar
The agent's first job is to know what is actually scheduled. The FXMacroData release calendar endpoint returns scheduled releases per currency, including indicator name, scheduled UTC datetime, prior value, and importance ranking when available.
Query it for each major currency you trade:
curl "https://api.fxmacrodata.com/v1/calendar/usd?api_key=YOUR_API_KEY"
curl "https://api.fxmacrodata.com/v1/calendar/eur?api_key=YOUR_API_KEY"
curl "https://api.fxmacrodata.com/v1/calendar/gbp?api_key=YOUR_API_KEY"
curl "https://api.fxmacrodata.com/v1/calendar/jpy?api_key=YOUR_API_KEY"
Wrap this in Python so the agent can iterate across a configurable currency list:
import os
import requests
from datetime import datetime, timedelta, timezone
from dotenv import load_dotenv
load_dotenv()
API = "https://api.fxmacrodata.com/v1"
KEY = os.environ["FXMD_API_KEY"]
CURRENCIES = ["usd", "eur", "gbp", "jpy", "aud", "cad", "chf", "nzd"]
def fxmd_get(path, **params):
r = requests.get(
f"{API}{path}",
params={"api_key": KEY, **params},
timeout=25,
)
r.raise_for_status()
return r.json()
def upcoming_releases(hours_ahead: int = 24):
now = datetime.now(timezone.utc)
cutoff = now + timedelta(hours=hours_ahead)
releases = []
for ccy in CURRENCIES:
try:
data = fxmd_get(f"/calendar/{ccy}").get("data", [])
except requests.HTTPError:
continue
for ev in data:
try:
ts = datetime.fromisoformat(
ev["announcement_datetime"].replace("Z", "+00:00")
)
except (KeyError, ValueError):
continue
if now <= ts <= cutoff:
releases.append({
"currency": ccy.upper(),
"indicator": ev.get("indicator") or ev.get("name"),
"scheduled_utc": ts.isoformat(),
"prior": ev.get("prior_value"),
"consensus": ev.get("consensus") or ev.get("market_consensus"),
"importance": ev.get("importance") or ev.get("impact"),
})
releases.sort(key=lambda r: r["scheduled_utc"])
return releases
Step 2: Rank releases by likely market impact
Not all prints move markets. Non-Farm Payrolls, Core PCE, Fed policy rate, euro-area CPI, UK CPI, and BoJ policy rate dominate FX flow. A retail-trade flash for a minor economy rarely matters.
Encode an explicit weight table so the agent does not have to guess. This is the single highest-leverage piece of the whole system — your weights, your edge.
TIER_1 = {
("USD", "non_farm_payrolls"), ("USD", "core_pce"), ("USD", "policy_rate"),
("USD", "inflation"), ("USD", "fomc_minutes"),
("EUR", "inflation"), ("EUR", "policy_rate"),
("GBP", "inflation"), ("GBP", "policy_rate"),
("JPY", "policy_rate"), ("JPY", "inflation"),
("AUD", "policy_rate"), ("CAD", "policy_rate"),
("CHF", "policy_rate"), ("NZD", "policy_rate"),
}
TIER_2 = {
("USD", "retail_sales"), ("USD", "ism_manufacturing"),
("EUR", "gdp"), ("EUR", "unemployment"),
("GBP", "gdp"), ("GBP", "retail_sales"),
("AUD", "inflation"), ("CAD", "inflation"),
}
def impact_score(event: dict) -> int:
key = (event["currency"], (event["indicator"] or "").lower())
if key in TIER_1:
return 3
if key in TIER_2:
return 2
return 1
def rank(releases):
return sorted(
releases,
key=lambda r: (-impact_score(r), r["scheduled_utc"]),
)
Now you can collapse the day into a structured list where tier-1 events surface first regardless of UTC time.
Step 3: Add overnight context so the agent knows what already moved
A morning briefing is incomplete without a snapshot of overnight FX moves. Pull the most recent spot rates for the pairs you care about so the LLM can weave price action into its narrative.
PAIRS = [("USD", "JPY"), ("EUR", "USD"), ("GBP", "USD"), ("AUD", "USD")]
def overnight_moves():
moves = []
for base, quote in PAIRS:
try:
data = fxmd_get(
"/forex",
base=base,
quote=quote,
).get("data", [])
except requests.HTTPError:
continue
if len(data) < 2:
continue
last = data[-1]["value"]
prev = data[-25]["value"] if len(data) >= 25 else data[0]["value"]
change_pct = (last - prev) / prev * 100
moves.append({
"pair": f"{base}/{quote}",
"last": round(last, 5),
"change_pct_24h": round(change_pct, 2),
})
return moves
If you also want positioning context for swing trades, add a COT pull here. Keep it optional — the briefing should still work without it.
Step 4: Generate the briefing with an LLM
The agent now has three structured inputs: ranked events, overnight FX moves, and the current UTC timestamp. Pass them into the model with a strict output contract so the briefing is parseable and consistent every morning.
import json
from anthropic import Anthropic
claude = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
SYSTEM_PROMPT = """You are an FX morning-prep analyst.
Given today's ranked releases and overnight moves, produce a briefing that:
- leads with the single most important event of the day,
- groups events by impact tier,
- flags 1-2 specific pairs to watch and why,
- ends with one disciplined risk caveat.
Do not give buy/sell instructions. Stay factual. Max 220 words."""
def generate_briefing(events, moves):
payload = json.dumps({
"utc_now": datetime.now(timezone.utc).isoformat(),
"ranked_events": events,
"overnight_moves": moves,
})
resp = claude.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=600,
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": payload}],
)
return resp.content[0].text
Three things make this prompt reliable:
- Structured input. The model receives JSON, not natural language, so it never has to extract dates or numbers from text.
- Hard scope. The system prompt forbids trade recommendations. That keeps the output compliant and useful even on volatile days.
- Length cap. 220 words forces signal density. Long briefings train you to skim, which defeats the purpose.
Step 5: Deliver to Telegram (or Slack)
The briefing needs to land where you already look first thing in the morning. Telegram is the fastest channel to set up.
def send_telegram(text: str):
token = os.environ["TELEGRAM_BOT_TOKEN"]
chat_id = os.environ["TELEGRAM_CHAT_ID"]
requests.post(
f"https://api.telegram.org/bot{token}/sendMessage",
json={
"chat_id": chat_id,
"text": text,
"parse_mode": "Markdown",
"disable_web_page_preview": True,
},
timeout=15,
)
def run():
events = rank(upcoming_releases(hours_ahead=24))
moves = overnight_moves()
briefing = generate_briefing(events, moves)
header = f"*FX Morning Briefing — {datetime.now(timezone.utc):%a %d %b %Y}*\n\n"
send_telegram(header + briefing)
if __name__ == "__main__":
run()
Schedule it. On Linux/macOS, a single crontab entry runs the agent every weekday at 06:30 UTC:
30 6 * * 1-5 /usr/bin/python3 /opt/fx-agent/morning_brief.py >> /var/log/fx-agent.log 2>&1
What the briefing actually looks like
Example output the agent produced on a recent CPI week:
FX Morning Briefing — Thu 22 May 2026
Today's anchor: US Core PCE at 12:30 UTC. Consensus 0.2% MoM,
prior 0.0%. A second sub-0.1 print would cement the disinflation
narrative; a 0.3+ surprise resets Fed pricing.
Tier 1:
- 12:30 UTC USD Core PCE prior +0.0% cons +0.2%
- 13:30 UTC USD Initial Jobless Claims prior 228k cons 225k
Tier 2:
- 06:00 UTC GBP Retail Sales MoM prior -0.1% cons +0.4%
- 09:00 UTC EUR ECB Minutes (qualitative)
Pairs to watch:
- USD/JPY hovering 158.40 after a quiet Asia. PCE miss → 156s
back in play.
- GBP/USD coiled below 1.2700. Stronger UK retail + soft PCE is
the cleanest setup of the day.
Risk caveat: thin EU liquidity ahead of US data; expect outsized
moves on any surprise print.
That is a complete pre-market read in under 90 seconds, every weekday, with no manual calendar scraping.
Hardening checklist before you trust it
- Stale data guard. Refuse to send the briefing if any tier-1 event is missing a scheduled time. Empty payload is better than a wrong one.
- Retry with backoff on transient HTTP failures. Three attempts, 2s/4s/8s, then fail loudly.
- Output validator. Reject any LLM response containing the words "buy", "sell", or "long" / "short". The agent's job is information, not execution.
- Heartbeat alert. If the agent fails to send a briefing by 07:00 UTC, ping yourself separately so you do not silently lose the workflow.
- Cost cap. Set
max_tokens=600and a daily LLM spend limit. Briefings should cost cents per day.
Where to take it next
The same scaffolding extends easily once the morning loop is reliable:
- Intraday surprise alerts. Poll announcements endpoints every 15 minutes after a tier-1 release; alert when actual diverges from consensus by more than your threshold.
- Pair-specific briefings. Generate a tighter USD/JPY-only or EUR/USD-only version for active days.
- Positioning overlay. Pull COT data weekly and feed it into the same prompt so the briefing knows when the market is one-sided.
- Voice mode. Pipe the briefing through a TTS model and have it read aloud while you make coffee.
The win is not the briefing itself — it is the disciplined, reproducible context you walk into your trading day with. Once that habit is automated, every other piece of FX agent infrastructure you build sits on top of it.