The Polling Tax: Real-Time Market Alerts With RSI, Bollinger Bands, and Telegram
Here's something most crypto and stock exchange documentation won't tell you upfront: candle history is almost always a plain REST endpoint, not a WebSocket feed.
Binance, Coinbase, Alpaca, Polygon — virtually every exchange exposes their OHLCV candle data as a standard JSON API you call with a GET request. There is no streaming endpoint for historical candles. You fetch, you get a snapshot, and that's it.
This creates an awkward problem the moment you want to build anything indicator-based. RSI and Bollinger Bands require a rolling window of past closes to compute. You can't derive them from a single tick. So the workflow becomes: fetch history, compute indicators, wait, fetch again, recompute. Over and over.
Most developers end up writing this themselves. It looks something like this:
// The polling loop most of us have written at least once
async function pollForever(symbol) {
while (true) {
try {
const candles = await fetchCandles(symbol); // REST call
const signal = computeIndicators(candles); // RSI, BB, etc.
if (signal) await sendAlert(signal);
} catch (err) {
console.error("Poll failed:", err.message);
}
await sleep(30_000); // hope nothing goes wrong while we wait
}
}
It works — until it doesn't. A failed request breaks the loop. A rate limit doesn't back off gracefully. Running this for five symbols means five independent loops with no shared error handling. And none of it is recoverable if the process restarts.
This is the polling tax: the infrastructure overhead you pay every time you want to turn a REST endpoint into something that behaves like a live feed.
This article shows how to eliminate that tax. You'll build a Node.js monitor that watches multiple coins or stocks, computes RSI and Bollinger Bands from live candle data, and fires a Telegram message the moment a threshold is crossed — with Restless Stream handling all the polling, reconnection, and delivery plumbing.
Here's what the output looks like:
BTCUSDT: RSI crossed above 70 (RSI=72.31, price=62500.00)
ETHUSDT: Price crossed below lower Bollinger Band (price=3120.50, lower=3132.12)
By the end, you'll understand the full architecture, where it breaks down, and how to point it at any candle provider — not just Binance.
Table of Contents
- The core problem: REST candles don't stream
- The architecture in plain terms
- Why Binance WebSockets alone aren't enough
- Where Restless Stream fits in
- What you'll need
- Step 1 — Pick the right candle endpoint
- Step 2 — Stream it with Restless Stream
- Step 3 — Build the full monitor
- Running the script
- Scaling up: more symbols, less bandwidth
- Using the same pattern for stocks
- When this approach is the wrong tool
- Troubleshooting common errors
- Conclusion
The core problem: REST candles don't stream
The polling loop above has five distinct failure modes, and most developers only discover them one by one in production:
- No backoff on errors. A single failed request throws and breaks the loop unless you wrap everything in retry logic.
- No rate limit awareness. Polling five symbols every 30 seconds is 10 requests per minute. Add more symbols or shorten the interval and you'll hit exchange rate limits with no handling in place.
- No reconnect semantics. If the process crashes and restarts, there's no concept of "resume" — you just start fresh and hope you didn't miss a crossing.
- Tight coupling between polling and business logic. The fetch, the indicator math, and the alerting are all tangled together, making each part harder to test or replace.
- No shared infrastructure across symbols. Each symbol is its own bespoke loop. Five symbols means five copies of the same fragile plumbing.
None of these are unsolvable. But solving them all yourself means your "quick monitor script" quietly becomes a polling framework. That's the tax.
The goal here is a push-based alert system built on three technical building blocks:
- Candle history (OHLCV data for indicator math)
- Polling + streaming (Restless Stream keeps data fresh without you writing retry logic)
- Telegram notifications (a chat message is the original push notification)
The architecture in plain terms
Before touching code, here's the data flow at a high level:
Binance REST API (klines)
↓
Restless Stream (Direct mode — polls on a schedule, pushes via WebSocket)
↓
Node.js monitor (RSI + Bollinger Bands computed from the candle window)
↓
Telegram bot (fires only on threshold crossings, not every poll)
Key Takeaway: Restless Stream sits in the middle as the polling and delivery layer. You write the what to do with the data part. It handles the keep the data coming part.
Why Binance WebSockets alone aren't enough
The Binance WebSocket API is excellent for real-time price ticks and live candle updates. But it has one fundamental limitation for indicator-based monitoring: it gives you the present, not the past.
RSI and Bollinger Bands are lagging indicators. They're mathematically defined over a window of historical closes. RSI(14) requires 14+ prior close prices. Bollinger Bands(20) need 20. You cannot compute either from a single live event.
This creates a bootstrap problem. Even if you connect to the Binance WebSocket immediately, you still need a REST call to backfill historical candles before your first indicator computation is valid. And after that, you're on the hook for:
- Polling cadence — how often do you re-fetch?
- Reconnects and retries — what happens when the connection drops?
- Rate limit handling — Binance throttles aggressive pollers
- Payload parsing — Binance klines return arrays, not labeled objects; your indicator library expects arrays of closes
That plumbing is real work. It's not hard, but it's also not the part of the problem you actually care about.
Where Restless Stream fits in
Restless Stream is a service that turns any JSON REST endpoint into a continuous stream.
In Direct mode, you pass the upstream URL as a query parameter. Restless Stream polls it on a schedule and pushes each result over SSE or WebSocket. You don't write a polling loop. You don't handle reconnects. You subscribe and receive.
The two transport options:
| Transport | Endpoint | Best for |
|---|---|---|
| Server-Sent Events (SSE) | https://stream.restlessapi.stream/ |
Browser clients, simple consumers |
| WebSocket | wss://stream.restlessapi.stream/ws |
Node.js, bidirectional needs |
The key parameters:
url— the upstream REST endpoint to poll (required)pollingInterval— how often to poll in seconds (default:30, range:5–3600)payloadMode—FULL_DATAsends the full response each time;JSON_PATCHsends diffs after the first eventjq— optional server-side filter to trim the payload before it reaches your client
For this script, WebSocket + FULL_DATA is the right choice. We need the full candle array to recompute indicators from scratch on each poll.
Reference: Request parameters | WebSocket docs | SSE docs
What you'll need
- Node.js 18+ (for the built-in
fetchAPI — nonode-fetchneeded) - A Telegram bot token and a target chat ID (instructions below)
- A list of symbols to monitor (e.g.,
BTCUSDT,ETHUSDT) - Optional: a Restless Stream API key for higher rate limits
Finding your Telegram chat ID
If you already have a chat ID, skip this.
- Open a chat with your bot and send any message (it just needs a message in the history).
- Call
getUpdatesand look formessage.chat.idin the response:
curl "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates"
The response is JSON. Find the result array, pick the first entry, and read message.chat.id. That's your chat ID.
Step 1 — Pick the right candle endpoint
For indicator computation, you need candles (OHLCV), not ticks. Binance exposes them via REST as "klines."
https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=30m&limit=200
A few decisions baked into this URL:
Why 30-minute candles? Short enough to catch intraday moves; long enough to avoid false positives from noise. 1-minute candles generate far more alerts than most traders want to act on.
Why limit=200? RSI(14) and Bollinger Bands(20) have warm-up periods — the first several results are mathematically unstable. Providing 200 candles gives the indicators plenty of runway before the values you actually act on. If you reduce your indicator windows, you can reduce this. If you increase them, increase limit proportionally.
The Binance kline format matters. Each candle is an array, not an object. Index 4 is the close price — this is what indicator libraries expect. Here's one candle:
[
1715000000000, // 0: open time (ms)
"62100.00", // 1: open
"62800.00", // 2: high
"61900.00", // 3: low
"62500.00", // 4: close ← this is what we use
"1234.56", // 5: volume
...
]
If you're monitoring stocks instead, swap this URL for your provider's candle endpoint. The rest of the pipeline is provider-agnostic.
Step 2 — Stream it with Restless Stream
Now we hand the REST URL to Restless Stream and receive a live WebSocket feed.
The connection URL is built by encoding the upstream URL as a query parameter. Use URLSearchParams rather than manual string concatenation — the upstream URL contains & characters that must be properly encoded:
const upstream = "https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=30m&limit=200";
const params = new URLSearchParams({
url: upstream, // Restless Stream will poll this URL
pollingInterval: "30", // poll every 30 seconds
payloadMode: "FULL_DATA" // send the full klines array on every event
});
const wsUrl = `wss://stream.restlessapi.stream/ws?${params.toString()}`;
Why URLSearchParams matters here: The upstream URL itself contains query parameters (symbol=, interval=, etc.). If you concatenate strings manually, those inner & characters break the outer query string. URLSearchParams percent-encodes the entire upstream URL as a single value, which is what Restless Stream expects.
What the stream sends back
Every WebSocket message is a JSON object with a type field:
// A normal data delivery
{ "type": "update", "data": [ [...], [...], ... ] }
// An error signal
{ "type": "error", "error": { "code": "UPSTREAM_429", "message": "..." } }
The data field on an "update" event contains exactly what the upstream REST endpoint returned — in this case, the full Binance klines array. Your code parses it the same way you'd parse a direct REST response.
Error events are signals, not socket closures. The connection stays open. Log them, increment a counter if you like, but don't reconnect on "error" — only reconnect on the "close" event.
Step 3 — Build the full monitor
Install dependencies
npm i ws technicalindicators
ws— the standard WebSocket client for Node.jstechnicalindicators— a well-maintained library implementing RSI, Bollinger Bands, and many other indicators. It expects plain arrays of numbers, which aligns perfectly with what we extract from Binance klines.
Create monitor.mjs
The full script follows. Read through the inline comments — the architectural decisions are more important than the syntax.
import WebSocket from "ws";
import { RSI, BollingerBands } from "technicalindicators";
// ─── Configuration ────────────────────────────────────────────────────────────
// All parameters are environment variables so you don't hardcode secrets
// and can reconfigure without editing the script.
const RESTLESS_BASE_WS = "wss://stream.restlessapi.stream/ws";
const BINANCE_BASE = "https://api.binance.com/api/v3/klines";
// Accept a comma-separated list of symbols from the environment.
// Normalise them immediately: trim whitespace, force uppercase, drop empties.
const SYMBOLS = (process.env.SYMBOLS ?? "BTCUSDT,ETHUSDT")
.split(",")
.map((s) => s.trim().toUpperCase())
.filter(Boolean);
const INTERVAL = process.env.INTERVAL ?? "30m";
const LIMIT = Number(process.env.LIMIT ?? "200");
const POLLING_INTERVAL_SECONDS = Number(process.env.POLLING_INTERVAL ?? "30");
// Indicator windows — keep these in sync with LIMIT.
// Rule of thumb: LIMIT should be at least 3× your largest window.
const RSI_PERIOD = Number(process.env.RSI_PERIOD ?? "14");
const BB_PERIOD = Number(process.env.BB_PERIOD ?? "20");
const BB_STDDEV = Number(process.env.BB_STDDEV ?? "2");
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN ?? "";
const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID ?? "";
const RESTLESS_API_KEY = process.env.RESTLESS_API_KEY; // optional
// Fail fast if Telegram credentials are missing — better than silent failure
// 10 minutes into a run.
if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
console.error("Missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID.");
process.exit(1);
}
// ─── URL Builders ─────────────────────────────────────────────────────────────
function buildBinanceKlinesUrl(symbol) {
const url = new URL(BINANCE_BASE);
url.searchParams.set("symbol", symbol);
url.searchParams.set("interval", INTERVAL);
url.searchParams.set("limit", String(LIMIT));
return url.toString();
}
function buildRestlessWsUrl(upstreamUrl) {
const params = new URLSearchParams({
url: upstreamUrl,
pollingInterval: String(POLLING_INTERVAL_SECONDS),
payloadMode: "FULL_DATA",
});
// Only set the key if it exists — avoids sending "apiKey=undefined".
if (RESTLESS_API_KEY) params.set("apiKey", RESTLESS_API_KEY);
return `${RESTLESS_BASE_WS}?${params.toString()}`;
}
// ─── Telegram ─────────────────────────────────────────────────────────────────
async function sendTelegram(text) {
const endpoint = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`;
const res = await fetch(endpoint, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
chat_id: TELEGRAM_CHAT_ID,
text,
disable_web_page_preview: true,
}),
});
if (!res.ok) {
// Don't throw — a Telegram failure shouldn't crash the monitor.
const body = await res.text();
console.error(`Telegram sendMessage failed: ${res.status} ${body}`);
}
}
// ─── Data Helpers ─────────────────────────────────────────────────────────────
// Binance returns each kline as an array. Index 4 is the close price.
// We extract close prices as plain numbers for the indicator library.
function toClosesFromBinanceKlines(klines) {
if (!Array.isArray(klines)) return [];
return klines
.map((k) => Number(k?.[4]))
.filter((n) => Number.isFinite(n)); // drop any malformed entries
}
function last(arr) {
return arr.length ? arr[arr.length - 1] : undefined;
}
// Format numbers for human-readable Telegram messages.
// Large prices (e.g., BTC at 60000) look fine with 2 decimal places.
// Small prices (e.g., altcoins at 0.0003) need more precision.
function formatNumber(n) {
if (!Number.isFinite(n)) return "n/a";
return Math.abs(n) >= 100 ? n.toFixed(2) : n.toFixed(6);
}
// ─── Indicator Signal Logic ────────────────────────────────────────────────────
// Classifies where the current price sits relative to the Bollinger Bands.
// Returns a string used as a state key for crossing detection.
function getBandState(price, bands) {
if (!bands || !Number.isFinite(price)) return "unknown";
if (price > bands.upper) return "aboveUpper";
if (price < bands.lower) return "belowLower";
return "inside";
}
// Detects threshold crossings between two consecutive values.
// A crossing only fires when prev was on one side and curr is on the other.
// This prevents repeated alerts when a value hovers near the threshold.
function crossesThreshold(prev, curr, threshold, direction) {
if (!Number.isFinite(prev) || !Number.isFinite(curr)) return false;
if (direction === "up") return prev < threshold && curr >= threshold;
if (direction === "down") return prev > threshold && curr <= threshold;
return false;
}
// ─── Per-Symbol Monitor ────────────────────────────────────────────────────────
function createSymbolMonitor(symbol) {
const upstream = buildBinanceKlinesUrl(symbol);
const wsUrl = buildRestlessWsUrl(upstream);
// Exponential backoff state for reconnects.
// Start at 1s, double on each failure, cap at 60s.
let backoffMs = 1_000;
// These track the previous poll's values so we can detect crossings.
// undefined means "no prior data yet" — we skip alert checks on the first poll.
let prevRsi = undefined;
let prevBandState = "unknown";
// Cooldown guard: prevents alert spam if a value flaps around a threshold.
// A real system might use a more sophisticated debounce, but this is sufficient
// for a 30-minute candle cadence.
let lastAlertAt = 0;
const cooldownMs = 30_000;
function log(...args) {
// Prefix all logs with the symbol so output is easy to filter.
console.log(`[${symbol}]`, ...args);
}
async function maybeAlert(message) {
const now = Date.now();
if (now - lastAlertAt < cooldownMs) return;
lastAlertAt = now;
log("Alert:", message);
await sendTelegram(message);
}
function connect() {
log("Connecting:", wsUrl);
const ws = new WebSocket(wsUrl);
ws.on("open", () => {
// Reset backoff on successful connection.
backoffMs = 1_000;
log("Connected.");
});
ws.on("message", async (raw) => {
let payload;
try {
payload = JSON.parse(raw.toString("utf8"));
} catch {
// Ignore non-JSON frames — Restless Stream shouldn't send them, but
// defensive parsing is cheap insurance.
log("Non-JSON message (ignored).");
return;
}
// Error events are signals: log them, but keep the connection open.
// The socket will close itself if the error is fatal.
if (payload?.type === "error") {
const code = payload?.error?.code ?? "unknown";
const msg = payload?.error?.message ?? "Unknown error";
log("Stream error event:", code, msg);
return;
}
if (payload?.type !== "update") return;
// Extract close prices from the raw Binance klines array.
const closes = toClosesFromBinanceKlines(payload.data);
// Guard: indicator libraries throw or return nonsense with too few inputs.
// The +1 accounts for the warm-up period in each indicator.
if (closes.length < Math.max(RSI_PERIOD + 1, BB_PERIOD + 1)) {
log("Not enough candles yet:", closes.length);
return;
}
// Compute indicators over the full window.
// Both functions return an array — the last element is the current value.
const rsiValues = RSI.calculate({ period: RSI_PERIOD, values: closes });
const bbValues = BollingerBands.calculate({
period: BB_PERIOD,
values: closes,
stdDev: BB_STDDEV,
});
const rsi = last(rsiValues);
const bands = last(bbValues); // { upper, middle, lower }
const price = last(closes);
// Bail if any computation produced an invalid result.
if (!Number.isFinite(rsi) || !bands || !Number.isFinite(price)) return;
const bandState = getBandState(price, bands);
// ── RSI crossing alerts ──────────────────────────────────────────────
// Only alert when prev was below the threshold and curr is above (or vice versa).
// Skip if prevRsi is undefined — that's the first poll, no prior state to compare.
if (prevRsi !== undefined) {
if (crossesThreshold(prevRsi, rsi, 70, "up")) {
await maybeAlert(
`${symbol}: RSI crossed above 70 (RSI=${formatNumber(rsi)}, price=${formatNumber(price)})`
);
}
if (crossesThreshold(prevRsi, rsi, 30, "down")) {
await maybeAlert(
`${symbol}: RSI crossed below 30 (RSI=${formatNumber(rsi)}, price=${formatNumber(price)})`
);
}
}
// ── Bollinger Band crossing alerts ───────────────────────────────────
// Alert when the band state changes — not just when price is outside the bands.
// State transitions: "inside" → "aboveUpper" or "inside" → "belowLower"
if (prevBandState !== "unknown" && prevBandState !== bandState) {
if (bandState === "aboveUpper") {
await maybeAlert(
`${symbol}: Price crossed above upper Bollinger Band (price=${formatNumber(price)}, upper=${formatNumber(bands.upper)})`
);
} else if (bandState === "belowLower") {
await maybeAlert(
`${symbol}: Price crossed below lower Bollinger Band (price=${formatNumber(price)}, lower=${formatNumber(bands.lower)})`
);
}
}
// Update state for the next poll.
prevRsi = rsi;
prevBandState = bandState;
log(
`RSI=${formatNumber(rsi)}, price=${formatNumber(price)}, bands=[${formatNumber(bands.lower)}, ${formatNumber(bands.upper)}], state=${bandState}`
);
});
ws.on("close", (code, reason) => {
log("Disconnected:", code, reason?.toString?.() ?? "");
// Exponential backoff: wait → reconnect → double wait → cap at 60s.
const wait = backoffMs;
backoffMs = Math.min(backoffMs * 2, 60_000);
setTimeout(connect, wait);
});
ws.on("error", (err) => {
// The "close" event fires after "error" in Node.js ws — reconnect logic
// lives there. This handler just logs.
log("WebSocket error:", err?.message ?? err);
});
}
connect();
}
// ─── Entry Point ──────────────────────────────────────────────────────────────
console.log("Starting monitors for:", SYMBOLS.join(", "));
for (const symbol of SYMBOLS) createSymbolMonitor(symbol);
Key architectural decisions worth understanding
One connection per symbol. Each call to createSymbolMonitor opens an independent WebSocket. This is intentional: connections are isolated, so a single symbol's errors or reconnects don't affect the others. At small scale (2–10 symbols), this is fine. At large scale, you'd multiplex onto fewer connections — but that adds complexity that isn't warranted here.
Crossing detection, not threshold detection. The crossesThreshold function fires only when prev was on one side of the threshold and curr is on the other. If RSI is sitting at 72 and stays there across multiple polls, you get exactly one alert. Without this, you'd get one per polling interval — which is noise, not signal.
State is preserved across polls. prevRsi and prevBandState are closured inside createSymbolMonitor so they persist between "message" events. The WebSocket connection maintains them for the lifetime of that connection.
Reconnect lives in "close", not "error". In Node.js's ws library, an "error" event is always followed by a "close" event. Putting reconnect logic in "error" would cause double-reconnects. The "close" handler is the single authoritative place to trigger a retry.
Running the script
export TELEGRAM_BOT_TOKEN="<your-telegram-bot-token>"
export TELEGRAM_CHAT_ID="<your-chat-id>"
# Optional — required only for higher limits or paid usage
export RESTLESS_API_KEY="<your-restless-stream-api-key>"
# Comma-separated list of Binance symbols
export SYMBOLS="BTCUSDT,ETHUSDT"
node monitor.mjs
Start with one symbol. Confirm you see [BTCUSDT] Connected. followed by regular log lines showing RSI and price values before adding more. It's much easier to debug a single stream.
You should see output every ~30 seconds (matching pollingInterval):
Starting monitors for: BTCUSDT, ETHUSDT
[BTCUSDT] Connecting: wss://stream.restlessapi.stream/ws?...
[ETHUSDT] Connecting: wss://stream.restlessapi.stream/ws?...
[BTCUSDT] Connected.
[ETHUSDT] Connected.
[BTCUSDT] RSI=58.42, price=62500.00, bands=[61200.00, 63800.00], state=inside
[ETHUSDT] RSI=44.11, price=3120.50, bands=[3090.00, 3145.00], state=inside
Scaling up: more symbols, less bandwidth
The one-connection-per-symbol model is clean and debuggable. For small watch lists it's perfectly adequate. When you push into dozens of symbols, a few knobs help:
| Lever | How it helps | Trade-off |
|---|---|---|
Increase pollingInterval |
Fewer upstream requests, lower rate limit exposure | Slightly delayed signal detection |
Reduce limit |
Smaller payload per poll | Fewer warm-up candles; reduce indicator windows in tandem |
payloadMode=JSON_PATCH |
Ships a diff after the first full payload | More complex client-side reconstruction |
jq server-side filter |
Trim the Binance response to only [*][4] (closes) before it leaves the server |
Reduces bandwidth; loses other fields if you later need them |
For 30-minute candles specifically, a pollingInterval of 300 seconds (5 minutes) is usually sufficient — a new candle only closes every 30 minutes, so polling faster than the candle interval is mostly redundant.
Using the same pattern for stocks
The shape of the problem doesn't change. You still need candle history plus live updates. The only thing that changes is the upstream URL.
// Alpha Vantage (stocks) — 30-minute interval
const upstream = `https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=AAPL&interval=30min&apikey=${API_KEY}`;
If your stock provider requires authentication headers (e.g., Authorization: Bearer ...), pass them through Direct mode using the headers query parameter — a URL-encoded JSON object:
const headers = encodeURIComponent(JSON.stringify({
Authorization: `Bearer ${STOCK_API_TOKEN}`
}));
const params = new URLSearchParams({
url: upstream,
pollingInterval: "60",
payloadMode: "FULL_DATA",
headers,
});
You'd also update toClosesFromBinanceKlines to match your provider's response shape — that function is the only provider-specific piece in the entire script.
Troubleshooting common errors
Stream errors arrive as "error" type events — they don't close the socket. The error.code field tells you what went wrong.
| Error code | Cause | Fix |
|---|---|---|
UPSTREAM_429 |
Binance rate-limited the poller | Increase pollingInterval or reduce limit |
bad_request |
Invalid parameters (usually encoding issues) | Check that your url parameter is properly encoded via URLSearchParams |
upstream_unavailable |
Binance temporarily unreachable | Transient — the monitor will recover automatically |
UPSTREAM_TIMEOUT |
Upstream took too long to respond | Usually transient; consider increasing pollingInterval |
For error codes not listed here, see the full error code reference.
The most common setup mistake is forgetting that URLSearchParams is necessary. Manually concatenating query strings with &url=https://api.binance.com/...&symbol=... breaks because the inner & terminates the outer url parameter prematurely. If you're seeing bad_request errors, this is the first thing to check.
Conclusion
You've built something small but meaningful: a market monitor that does exactly one job well. It watches symbols, computes indicators from live candle history, and alerts you precisely when a threshold is crossed — not every 30 seconds, not when you remember to check a dashboard.
A few things to carry forward:
- The crossing detection pattern is reusable. Any two-state threshold (MACD crossover, volume spike, moving average cross) works the same way: track
prev, compare tocurr, fire once on transition. - Restless Stream's Direct mode removes the polling plumbing. That's infrastructure that rarely adds business value to write yourself.
- The design scales by substitution. Swap the upstream URL for any candle provider. Swap Telegram for Slack, PagerDuty, or a webhook. The indicator and alert logic doesn't care.
The next natural extensions: persist state to survive restarts, add a /status HTTP endpoint to check indicator values on demand, or compose multiple signals into a single composite alert (RSI overbought and price above the upper band simultaneously).
Safety note: This article is for educational purposes only. Nothing here constitutes financial advice.
Happy building.
Built with Node.js 18+, ws, technicalindicators, and Restless Stream.

0 Comments: