How to Build a Financial Newsletter with Automated Market Data
Create an automated financial newsletter that pulls live indicator data, generates market commentary, and sends to subscribers on a schedule.
The Newsletter Business Model
Financial newsletters are one of the most profitable content businesses. The key challenge is producing consistent, data-driven content on a schedule. By automating the data collection and initial analysis, you can focus on the editorial -- the insights that subscribers actually pay for.
This tutorial builds a system that: (1) pulls market data from the TickAtlas API, (2) generates a structured market brief, (3) formats it as an email, and (4) sends it via an email service.
Architecture
Cron Job (daily at 6:00 AM UTC)
|
v
Python Script:
1. Fetch indicators for watchlist (TickAtlas API)
2. Identify notable signals
3. Generate market summary
4. Format as HTML email
5. Send via SendGrid/Mailgun
Step 1: Fetch Market Data
import requests
from dataclasses import dataclass, field
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://tickatlas.com/v1"
WATCHLIST = ["EURUSD", "GBPUSD", "USDJPY", "XAUUSD", "BTCUSD"]
@dataclass
class SymbolBrief:
symbol: str
close: float
rsi: float
macd_signal: str
trend: str
notable: bool = False
note: str = ""
def fetch_symbol_brief(symbol: str) -> SymbolBrief:
indicators = {}
ohlcv = {}
for ind in ["RSI_14", "MACD", "EMA_50"]:
resp = requests.get(
f"{BASE_URL}/indicator",
headers={"X-API-Key": API_KEY},
params={"symbol": symbol, "indicator": ind, "timeframe": "D1"},
)
data = resp.json()["data"]
indicators[ind] = data["values"]
indicators[f"{ind}_signal"] = data.get("signal", "neutral")
if not ohlcv:
ohlcv = data["ohlcv"]
close = ohlcv["close"]
rsi = indicators["RSI_14"]["rsi"]
ema = indicators["EMA_50"]["value"]
trend = "bullish" if close > ema else "bearish"
brief = SymbolBrief(
symbol=symbol,
close=close,
rsi=rsi,
macd_signal=indicators["MACD_signal"],
trend=trend,
)
# Flag notable conditions
if rsi > 75 or rsi < 25:
brief.notable = True
brief.note = f"RSI at {rsi:.1f} -- {'overbought' if rsi > 75 else 'oversold'}"
return brief
briefs = [fetch_symbol_brief(sym) for sym in WATCHLIST] Step 2: Generate the Market Summary
from datetime import datetime
def generate_summary(briefs: list[SymbolBrief]) -> str:
"""Generate a plain-text market summary."""
date = datetime.utcnow().strftime("%B %d, %Y")
lines = [f"Market Brief -- {date}", "=" * 40, ""]
# Notable signals first
notable = [b for b in briefs if b.notable]
if notable:
lines.append("NOTABLE SIGNALS:")
for b in notable:
lines.append(f" {b.symbol}: {b.note}")
lines.append("")
# Full watchlist
lines.append("WATCHLIST OVERVIEW:")
for b in briefs:
arrow = "^" if b.trend == "bullish" else "v"
lines.append(
f" {b.symbol}: {b.close:.5f} {arrow} | "
f"RSI: {b.rsi:.1f} | MACD: {b.macd_signal} | "
f"Trend: {b.trend}"
)
return "\n".join(lines) Step 3: Format as HTML Email
def format_html_email(briefs: list[SymbolBrief]) -> str:
date = datetime.utcnow().strftime("%B %d, %Y")
rows = ""
for b in briefs:
color = "#22c55e" if b.trend == "bullish" else "#ef4444"
highlight = ' style="background:#fef3c7;"' if b.notable else ""
rows += f"""
<tr{highlight}>
<td style="padding:8px;font-weight:bold;">{b.symbol}</td>
<td style="padding:8px;">{b.close:.5f}</td>
<td style="padding:8px;">{b.rsi:.1f}</td>
<td style="padding:8px;color:{color};">{b.trend}</td>
<td style="padding:8px;">{b.note or '-'}</td>
</tr>"""
return f"""
<div style="font-family:sans-serif;max-width:600px;margin:0 auto;">
<h1 style="color:#1a1a2e;">Market Brief</h1>
<p style="color:#666;">{date}</p>
<table style="width:100%;border-collapse:collapse;border:1px solid #ddd;">
<tr style="background:#f8f9fa;">
<th style="padding:8px;text-align:left;">Symbol</th>
<th style="padding:8px;">Price</th>
<th style="padding:8px;">RSI</th>
<th style="padding:8px;">Trend</th>
<th style="padding:8px;">Note</th>
</tr>
{rows}
</table>
<p style="color:#999;font-size:12px;margin-top:20px;">
Data sourced from TickAtlas API. Not financial advice.
</p>
</div>""" Step 4: Send via Email Service
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
def send_newsletter(html_content: str, recipients: list[str]):
msg = MIMEMultipart("alternative")
msg["Subject"] = f"Market Brief - {datetime.utcnow().strftime('%b %d')}"
msg["From"] = "[email protected]"
msg.attach(MIMEText(html_content, "html"))
with smtplib.SMTP("smtp.sendgrid.net", 587) as server:
server.starttls()
server.login("apikey", "YOUR_SENDGRID_KEY")
for recipient in recipients:
msg["To"] = recipient
server.sendmail(msg["From"], recipient, msg.as_string())
# Full pipeline
briefs = [fetch_symbol_brief(sym) for sym in WATCHLIST]
html = format_html_email(briefs)
send_newsletter(html, ["[email protected]"]) Scaling Ideas
Add AI commentary
Feed the market data to an LLM and generate a paragraph of analysis for each notable signal. See our prompt engineering guide.
Personalized watchlists
Let subscribers choose which symbols they want in their brief. Store preferences in a database and generate per-user content.
Alert-based extras
Send intraday alerts when RSI hits extreme levels. Use the same API calls but trigger on threshold breaches instead of a schedule.
Related Reading
Try this with live data
Every account gets $2.50 in free PAYG credits. No card required — paste your API key and run the code above against live broker data.
Keep reading
All articles- Tutorial 11 min read
24/7 Crypto Monitoring: Building Always-On Analysis Systems
Build a monitoring system that watches crypto markets around the clock, detects significant moves, and sends alerts when conditions match your criteria.
March 28, 2026
- Tutorial 12 min read
How to Build an AI Market Analyst That Runs 24/7
Build a production-ready AI market analyst that monitors forex and crypto markets around the clock, generates daily briefings, and alerts you to opportunities via Telegram.
March 28, 2026
- Tutorial 10 min read
Using ATR for Dynamic Stop-Loss Placement
Learn how to use Average True Range (ATR) to set volatility-adjusted stop losses that adapt to market conditions, with full code examples.
March 28, 2026