Ask a chatbot "what does the AAPL chart look like?" and you'll often get a confident answer with an RSI that doesn't match any chart you can find. Language models are good at explaining numbers and bad at calculating them from memory or a screenshot.
So split the job. Let code do the maths, let the model do the words. This post builds a Telegram bot in n8n that answers /ta AAPL with a price chart and a plain-English technical read, where RSI, MACD and the moving averages are computed in a Code node from real daily prices, and Google Gemini (free tier) only explains them. No paid market-data or charting API.
Not financial advice, and the bot says so in every reply.
What you end up with
Send the bot any of these:
/ta AAPL
$TSLA
VOD.L
BTC
^GSPC
and you get back a chart image (price with 20/50/200-day moving averages) plus a short report: trend, momentum, the signals that fired recently (golden cross, RSI over 70, MACD crossover...), an overall technical bias and what would change the picture. /start or /help explains the commands, and an unknown ticker gets a helpful reply instead of an error.
The architecture
Telegram Trigger
-> Code: read the ticker (/ta AAPL, $TSLA, bare ticker, aliases like BTC -> BTC-USD)
-> IF ticker given? (no -> send help)
-> HTTP: Yahoo Finance chart endpoint, 2 years of daily candles
-> Code: compute indicators + signals + bias score + chart config
-> IF price data found? (no -> explain the problem)
-> HTTP: QuickChart renders the chart PNG
-> Basic LLM Chain + Gemini + Structured Output Parser: explain the numbers
-> Telegram: send chart, then the analysis
Step 1: Parse the ticker forgivingly
People type tickers in every format, so the first Code node normalises them and maps a few friendly aliases:
const ALIAS = { BTC: 'BTC-USD', ETH: 'ETH-USD', SOL: 'SOL-USD', SPX: '^GSPC', SP500: '^GSPC',
NASDAQ: '^IXIC', FTSE: '^FTSE', GOLD: 'GC=F' };
const words = raw.replace(/^\/(\w+)@\w+/, '/$1').split(/\s+/).filter(Boolean);
const first = (words[0] || '').toLowerCase();
const args = first.startsWith('/') ? words.slice(1) : words;
let t = (args[0] || '').replace(/^\$/, '').toUpperCase();
t = ALIAS[t] || t;
const ok = !['/start', '/help'].includes(first) && /^[A-Z0-9.^=\-]{1,15}$/.test(t);
The /(\w+)@\w+ replace handles group chats, where Telegram sends /ta@YourBot AAPL.
Step 2: Get daily candles from Yahoo
The HTTP Request node calls Yahoo Finance's public chart endpoint:
https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?range=2y&interval=1d&includePrePost=false
Two years of daily data is enough for a 200-day moving average with room to spare. The node retries on failure and never throws, so a bad symbol turns into a friendly message in the next step rather than a failed execution. It's an unofficial endpoint, so treat it as fine for a personal or community bot and swap in a licensed data source for anything commercial.
Step 3: Do the maths in code
The indicator Code node is plain JavaScript with small helpers. Simple and exponential moving averages:
const sma = (a, n) => a.map((_, i) => i < n - 1 ? null : a.slice(i - n + 1, i + 1).reduce((s, v) => s + v, 0) / n);
const ema = (a, n) => { const k = 2 / (n + 1); let e = null; return a.map((v, i) => {
if (i < n - 1) return null; if (e === null) { e = a.slice(0, n).reduce((s, x) => s + x, 0) / n; return e; }
e = v * k + e * (1 - k); return e; }); };
RSI(14) with Wilder's smoothing (the version charting sites use), and MACD as EMA(12) minus EMA(26) with a 9-period signal line. Rows with missing prices are dropped first, and anything with fewer than 35 trading days gets a clear "not enough data" reply.
From those series the node derives:
- trend: price vs the 50 and 200-day averages, and the 20-day vs the 50-day
- signals from recent sessions: golden and death crosses (last 10 sessions), RSI above 70 or below 30, MACD crossing its signal line (last 3 sessions)
- a technical bias score that adds and subtracts points for each condition, so the label (bullish, neutral, bearish) is reproducible and not a mood
It also builds a Chart.js config for QuickChart, so the image matches the numbers exactly.
Step 4: Let Gemini explain, not calculate
The model receives the computed indicator values and signals, never the raw candle series, with instructions to explain them for a non-expert, use only the numbers given, say so when indicators disagree, and never tell the reader to buy, sell or hold. A Structured Output Parser returns the summary in fixed fields, and a Code node formats the final Telegram message with the not-financial-advice line.
Because the numbers come from code, you can check any report against TradingView and they agree, which is the whole point.
Running it
- Create a bot with @botfather and add a Telegram credential.
- Add a free Google Gemini API key from Google AI Studio.
- Import the workflow (link below), activate it, and send
/startto your bot.
Telegram triggers need a public HTTPS URL, so use n8n Cloud or a self-hosted instance with a domain or tunnel.
Free workflow and Pro version
The free workflow is in my GitHub repo of n8n templates: https://github.com/OlsOls4/n8n-ai-workflow-templates/tree/main/templates/stock-technical-analysis-telegram
There's also a Pro version for trading groups and newsletters: several tickers per message, Bollinger Bands, ATR and support/resistance zones, a /watch list kept in Google Sheets, a weekday evening digest with charts for anything that flagged a signal, a signals log and an allow-list of chats: https://payhip.com/b/fhSzZ (more templates: https://payhip.com/OllysAutomationTemplates).
If you add your own indicator to the Code node, I'd like to hear which one in the comments.
Top comments (0)