Tutorial 4: Connecting to a Market Data API (JavaScript)
Learn how to fetch fresh OHLCV data from a public API, convert it into plain arrays, and analyze it with ti-engine.
This workflow enables automated, up-to-date, and repeatable research pipelines β without CSVs.
Series so far:
- 01 - Getting started with ti-engine
- 02 - Plotting your indicators
- 03 - More advanced use cases
- [04 - Getting data from an API] (this file)
- 05 - Real-time updates with WebSockets (single vs bulk)
π― Goalβ
- Fetch OHLCV (Open, High, Low, Close, Volume) data from a free market data API
- Parse JSON into arrays suitable for ti-engine
- Run your indicator pipeline on fresh data (SMA, Bands, RSI, ATR)
- Understand alignment for rolling outputs
- Optional: wire results into the Plotly dashboard from Tutorial 2
π¦ Prerequisitesβ
- JavaScript runtime:
- Browser: modern browser with ESM modules
- Node.js: v18+ (has global fetch). For older Node, install node-fetch.
- Packages:
npm install ti-engine
# If plotting later:
# npm install plotly.js-dist-min
Initialize ti-engine (required in browsers; no-op in Node):
import init from "ti-engine";
await init();
π Using Binanceβs free market data APIβ
Weβll use Binanceβs public API for OHLCV βklinesβ (no API key required).
Docs: https://binance-docs.github.io/apidocs/spot/en/#kline-candlestick-data
Example endpoint (daily BTCUSDT, 365 bars):
https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1d&limit=365
- Replace
BTCUSDT
with another supported symbol. interval
can be1d
,1h
, etc.limit
controls number of bars.
Note: Some environments may encounter CORS restrictions for browser-based fetches. If so, use Node, a proxy, or your own backend.
π§βπ» Fetch OHLCV and map to arraysβ
// api-connection.mjs
import init, {
movingAverage,
momentumIndicators,
candleIndicators,
otherIndicators,
ConstantModelType,
DeviationModel,
MovingAverageType
} from "ti-engine";
await init();
/**
* Fetch OHLCV from Binance and map into arrays.
* @param {string} symbol e.g., "BTCUSDT"
* @param {string} interval e.g., "1d", "1h"
* @param {number} limit number of bars (<= 1500 typically)
* @returns {Promise<{date:string[], open:number[], high:number[], low:number[], close:number[], volume:number[]}>}
*/
async function fetchBinanceKlines(symbol = "BTCUSDT", interval = "1d", limit = 365) {
const url = new URL("https://api.binance.com/api/v3/klines");
url.searchParams.set("symbol", symbol);
url.searchParams.set("interval", interval);
url.searchParams.set("limit", String(limit));
const resp = await fetch(url);
if (!resp.ok) {
throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
}
const raw = await resp.json();
// Each row: [OpenTime, Open, High, Low, Close, Volume, CloseTime, ...]
const date = [];
const open = [];
const high = [];
const low = [];
const close = [];
const volume = [];
for (const row of raw) {
const openTime = row[0];
date.push(new Date(openTime).toISOString()); // ISO datetime; use .slice(0,10) for YYYY-MM-DD
open.push(parseFloat(row[1]));
high.push(parseFloat(row[2]));
low.push(parseFloat(row[3]));
close.push(parseFloat(row[4]));
volume.push(parseFloat(row[5]));
}
return { date, open, high, low, close, volume };
}
const { date, open, high, low, close } = await fetchBinanceKlines("BTCUSDT", "1d", 365);
console.log(`Fetched ${close.length} bars for BTCUSDT`);
βοΈ Run indicators on fresh dataβ
Weβll compute:
- SMA(20)
- Moving Constant Bands (EMA + StdDev, x2, period 20)
- RSI(20) with SmoothedMovingAverage
- ATR(14) with ExponentialMovingAverage
const smaPeriod = 20;
const sma20 = movingAverage.bulk.movingAverage(
close,
MovingAverageType.Simple,
smaPeriod
);
// Moving Constant Bands
const bandsPeriod = 20;
const bands = candleIndicators.bulk.movingConstantBands(
close,
ConstantModelType.ExponentialMovingAverage,
DeviationModel.StandardDeviation,
2.0,
bandsPeriod
);
const mcbLower = bands.map(t => t[0]);
const mcbMid = bands.map(t => t[1]);
const mcbUpper = bands.map(t => t[2]);
// RSI
const rsiPeriod = 20;
const rsi20 = momentumIndicators.bulk.relativeStrengthIndex(
close,
ConstantModelType.SmoothedMovingAverage,
rsiPeriod
);
// ATR
const atrPeriod = 14;
const atr14 = otherIndicators.bulk.averageTrueRange(
close, // previous closes
high,
low,
ConstantModelType.ExponentialMovingAverage,
atrPeriod
);
// Alignment: rolling outputs are shorter. Right-align to dates:
const tail = (xs, n) => xs.slice(xs.length - n);
const xSMA = tail(date, sma20.length);
const xBands = tail(date, bands.length);
const xRSI = tail(date, rsi20.length);
const xATR = tail(date, atr14.length);
// Inspect latest values
console.log("Latest SMA20:", sma20[sma20.length - 1]);
console.log("Latest Bands:", {
lower: mcbLower[mcbLower.length - 1],
mid: mcbMid[mcbMid.length - 1],
upper: mcbUpper[mcbUpper.length - 1]
});
console.log("Latest RSI20:", rsi20[rsi20.length - 1]);
console.log("Latest ATR14:", atr14[atr14.length - 1]);
π Visualize (optional)β
Reuse the Plotly layout from Tutorial 2.
At minimum, replace the hardcoded arrays there with your freshly fetched arrays here:
- Candles: x=date, open/open, high/high, low/low, close/close
- SMA trace: x=xSMA, y=sma20
- Bands traces: x=xBands with mcbUpper/mcbLower fill and mcbMid line
- RSI trace: x=xRSI, y=rsi20 (+ horizontal shapes at 70/30)
- ATR trace: x=xATR, y=atr14
See: Tutorial 2: Visualizing Indicators with Plotly (JavaScript)
π Real-time and incremental updates (optional)β
When a new bar arrives, you can compute βsingleβ values on the last N samples without recomputing the entire history:
import { momentumIndicators, otherIndicators, candleIndicators, ConstantModelType, DeviationModel } from "ti-engine";
// Example: update RSI(20), ATR(14), and Bands(20) on the latest bar
function computeLatestSnapshots(close, high, low) {
const latestRSI = momentumIndicators.single.relativeStrengthIndex(
close.slice(-rsiPeriod),
ConstantModelType.SmoothedMovingAverage
);
const latestATR = otherIndicators.single.averageTrueRange(
close.slice(-atrPeriod), // previous closes for the window
high.slice(-atrPeriod),
low.slice(-atrPeriod),
ConstantModelType.ExponentialMovingAverage
);
const [lower, mid, upper] = candleIndicators.single.movingConstantBands(
close.slice(-bandsPeriod),
ConstantModelType.ExponentialMovingAverage,
DeviationModel.StandardDeviation,
2.0
);
return { latestRSI, latestATR, bands: { lower, mid, upper } };
}
Note: For true live streaming, also handle partial candles (intra-bar) vs. closed candles and consider debouncing update frequency.
π§ͺ Handling alignment and edge casesβ
- Rolling functions return arrays of length L - N + 1 (L = input length, N = period).
- Right-align x-axis values with the tail of your date/time array.
- Ensure arrays (open, high, low, close) have identical lengths.
- For very small histories (length < period), compute only single values on available windows.
π‘οΈ Disclaimerβ
- Respect API rate limits and terms of use.
- Add retries and error handling for production usage.
- This example is educational; validate indicators and assumptions before using in live systems.
Happy fetching! π¦ππ