This is a companion piece to our guide on scraping currency exchange rates. A lot of it carries over almost unchanged — especially the data-type discipline (never a float for prices) and the caching practices. But stock quotes have their own quirks: tickers and exchanges, trading sessions and market holidays, corporate actions (splits and dividends), and — the single biggest difference from central-bank FX rates — data licensing. Real-time market data is regulated by the exchanges and by bodies like the SEC and FINRA. Unlike a freely published reference rate, you often can't pull it for free, and you almost never have the right to redistribute it.
1. What a "quote" actually is, and which data types exist
"Quote" means different things depending on the task, and pinning that down is the first thing you need to do:
- Last / current price — the price of the most recent trade. This is what the "share price now" widget shows.
- OHLCV bar (candle) — Open, High, Low, Close and Volume over an interval (minute, hour, day). The basis of charts and backtests.
- Bid/Ask (order book) — the best buy and sell prices. Needed for trading; usually the most expensive and heavily licensed data.
- EOD (end-of-day) — the day's final closing prices. Cheap or free, and fine for analytics and portfolio accounting.
- Adjusted close — the closing price corrected for splits and dividends so the history stays continuous.
And the key axis is freshness: real-time → 15–20 minute delay → end-of-day. The fresher and more granular the data, the stricter the license and the higher the price.
2. Exchange-native data and official sources
The gold standard is data straight from the exchange — but for equities that is almost always licensed and paid. In the US, real-time consolidated quotes flow through the SIP (Securities Information Processor) and the exchanges' own feeds, and are resold by authorized vendors under a market-data agreement. The same pattern holds for the LSE in the UK, the ASX in Australia and the TSX in Canada. What you can usually get for free is delayed (about 15 minutes) or end-of-day data.
| Exchange | What it covers | Access |
|---|---|---|
| NYSE / Nasdaq (US) | US equities, ETFs, ADRs | Real-time via the SIP / exchange feeds and authorized vendors (paid, licensed); 15-min delayed and EOD widely available free |
| LSE (UK) | UK and international equities | Real-time licensed via LSE / vendors; delayed and EOD via aggregators |
| ASX (Australia) | Australian equities | Real-time licensed; delayed and EOD via vendors |
| TSX (Canada) | Canadian equities | Real-time licensed; delayed and EOD via vendors |
| Nasdaq Data Link (formerly Quandl) | historical EOD, fundamentals, alternative data | REST API in JSON/XML/CSV; free tier with a key, premium datasets paid |
A developer-friendly source for historical data is Nasdaq Data Link (formerly Quandl), a REST service owned by Nasdaq that returns end-of-day prices and fundamentals as JSON, XML or CSV.
A quirk of the Nasdaq Data Link format: data comes back "column-oriented" — a separate array of column names (
column_names) and an array of rows (data). To pull a field you have to map the column name to its index rather than relying on a fixed order (see the PHP example below). Several exchange and vendor APIs use this same shape, so it's worth learning once.Handy bridge to the FX guide: many of the aggregators below (Finnhub, Twelve Data, Alpha Vantage) return not only equities but also FX pairs and crypto. One integration can cover both stocks and live exchange rates — the market rate, not an official central-bank reference rate.
3. Global market-data APIs
There is no fully open "just hit the endpoint" source here the way there is for a central-bank FX feed: they all use a key and enforce limits, and real-time is almost always paid.
| Service | Coverage | Key | Free limit | Notes |
|---|---|---|---|---|
| Nasdaq Data Link | historical EOD, fundamentals | needed | tiered | strong for bulk history; columnar JSON |
| Finnhub | US and international equities, FX, crypto | needed | ~60 req/min | has a WebSocket; history limited on free |
| Twelve Data | equities, FX, crypto | needed | ~800 req/day | OHLC, indicators, clean REST |
| Alpha Vantage | 200k+ tickers, 20+ exchanges | needed | 25 req/day (5/min) | EOD and indicators; real-time US is paid |
| yfinance (unofficial Yahoo) | very broad | not needed | no hard limit, but it's scraping | for prototypes and learning, not production |
| EODHD | 150k+ tickers globally | needed | trial | strong at bulk history export |
| Financial Modeling Prep | prices + fundamentals | needed | limited | statements, multiples |
| Tiingo | EOD + US fundamentals | needed | limited | clean end-of-day |
| Marketstack / Polygon.io | global / real-time US | needed | trial / effectively paid | Polygon: low latency, tick data |
A current detail worth flagging: IEX Cloud shut down on August 31, 2024. If some older guide still recommends it, that's out of date — the nearest replacements are Alpha Vantage and Financial Modeling Prep.
Free tiers are great for a prototype but hit their limits fast (Alpha Vantage's 25 requests a day is literally a couple dozen tickers). So on a free tier you always build the logic around a cache and local storage: pull the history once, then only append fresh points.
4. Why stock quotes are harder than FX rates
A few differences that break a naive scraper:
- A ticker isn't unique. The same symbol can trade on several exchanges. So the instrument key is the pair "exchange + ticker" — and more reliably, an international identifier like ISIN or FIGI.
- Trading sessions and holidays. Every exchange has its own schedule, time zone, and pre- and post-market sessions. The "last price" over a weekend is Friday's price.
- Corporate actions. A 1:10 split "drops" the price tenfold — but that's not a market crash, it's a technical recalculation. This is the direct analog of the denomination trap when scraping FX rates: ignore it and you get a false anomaly. Dividends and splits are folded into the adjusted close.
- Instrument currency. A security's price is quoted in the exchange's currency (USD, GBP, EUR, JPY…), and to hold a portfolio in one base currency you have to convert — which is exactly where the rates from the companion article come in.
- Volume. It's an integer, but a very large one — millions and billions of shares. It needs a 64-bit integer type.
5. Five solutions in different languages
Each example hits a different source to show off different formats. Throughout, the accent is on the two rules from the FX guide: prices are stored in a decimal type (never float), and volume in an integer type.
5.1. PHP — Nasdaq Data Link (columnar JSON)
<?php
declare(strict_types=1);
/**
* Latest close from a Nasdaq Data Link time-series dataset.
* Data comes back "column-oriented": column_names + rows.
*/
function nasdaqLastClose(string $database, string $code): ?string
{
$key = getenv('NASDAQ_DATA_LINK_KEY');
$url = sprintf(
'https://data.nasdaq.com/api/v3/datasets/%s/%s.json?rows=1&api_key=%s',
rawurlencode($database),
rawurlencode($code),
rawurlencode($key)
);
$raw = file_get_contents($url);
if ($raw === false) {
throw new RuntimeException('Failed to fetch Nasdaq Data Link');
}
$json = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
// Columnar payload: names in one array, values in another
$columns = $json['dataset']['column_names'] ?? [];
$rows = $json['dataset']['data'] ?? [];
if (!$rows) {
return null; // no data or bad code
}
$idx = array_flip($columns); // column name -> index
$close = $rows[0][$idx['Close']] ?? null; // latest close
return $close === null ? null : (string) $close;
}
echo 'Close: ' . (nasdaqLastClose('WIKI', 'AAPL') ?? 'no data') . " USD\n";
The instructive part is array_flip($columns) — mapping a column name to its index. That's the correct way to read a columnar API: field order isn't guaranteed, and "magic" indexes like [4] break the moment the response changes. For bit-exact prices, prefer requesting CSV or keeping the raw text — the JSON decoder has already turned the number into a float.
5.2. Python — Finnhub (current quote, Decimal)
import os
import requests
from decimal import Decimal
FINNHUB_TOKEN = os.environ["FINNHUB_TOKEN"] # free key, ~60 req/min
def finnhub_quote(symbol: str) -> dict[str, Decimal]:
"""Current quote for a symbol (US market on the free tier)."""
resp = requests.get(
"https://finnhub.io/api/v1/quote",
params={"symbol": symbol, "token": FINNHUB_TOKEN},
timeout=10,
)
resp.raise_for_status()
d = resp.json()
# Decimal(str(...)) pins exactly the value that arrived in the JSON
return {
"current": Decimal(str(d["c"])), # current price
"open": Decimal(str(d["o"])),
"high": Decimal(str(d["h"])),
"low": Decimal(str(d["l"])),
"prev_close": Decimal(str(d["pc"])),
}
if __name__ == "__main__":
q = finnhub_quote("AAPL")
print(f"AAPL: {q['current']} USD (open {q['open']}, high {q['high']})")
5.3. JavaScript / Node.js — Twelve Data (OHLC, volume)
// Free: ~800 requests/day. Key required.
const API_KEY = process.env.TWELVE_DATA_KEY;
async function twelveQuote(symbol) {
const url = new URL("https://api.twelvedata.com/quote");
url.searchParams.set("symbol", symbol);
url.searchParams.set("apikey", API_KEY);
const res = await fetch(url);
const d = await res.json();
if (d.status === "error") throw new Error(d.message);
return {
symbol: d.symbol,
open: d.open, // strings — don't coerce to Number needlessly
high: d.high,
low: d.low,
close: d.close,
volume: d.volume, // volume is a big integer, keep it a string/BigInt
exchange: d.exchange,
};
}
twelveQuote("MSFT").then((q) =>
console.log(`${q.symbol} (${q.exchange}): close ${q.close}, vol ${q.volume}`)
);
JavaScript has no decimal type, and Number is a double. So prices are left as strings; for arithmetic on prices reach for decimal.js / big.js, and for volume use the native BigInt.
5.4. Go — Alpha Vantage (daily OHLCV candles)
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"sort"
"time"
)
// Alpha Vantage: free 25 req/day, 5/min.
type avDaily struct {
Series map[string]struct {
Open string `json:"1. open"`
High string `json:"2. high"`
Low string `json:"3. low"`
Close string `json:"4. close"`
Volume string `json:"5. volume"`
} `json:"Time Series (Daily)"`
}
func dailyBars(symbol string) (avDaily, error) {
key := os.Getenv("ALPHAVANTAGE_KEY")
url := fmt.Sprintf(
"https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol=%s&apikey=%s",
symbol, key,
)
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Get(url)
if err != nil {
return avDaily{}, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var out avDaily
if err := json.Unmarshal(body, &out); err != nil {
return avDaily{}, err
}
return out, nil
}
func main() {
bars, err := dailyBars("IBM")
if err != nil {
panic(err)
}
// find the most recent candle by date
dates := make([]string, 0, len(bars.Series))
for d := range bars.Series {
dates = append(dates, d)
}
sort.Strings(dates)
last := dates[len(dates)-1]
b := bars.Series[last]
// prices kept as strings; for math use shopspring/decimal
fmt.Printf("IBM %s: O=%s H=%s L=%s C=%s V=%s\n",
last, b.Open, b.High, b.Low, b.Close, b.Volume)
}
Conveniently, Alpha Vantage returns prices as strings — decimal precision survives out of the box, as long as you don't turn them into float64.
5.5. C# / .NET — Yahoo Finance (unofficial endpoint, decimal)
using System.Text.Json;
// ⚠️ Unofficial Yahoo Finance endpoint — the same one the yfinance library uses.
// Fine for prototypes and learning, but with no guarantees and NOT for production.
public static class YahooChart
{
private static readonly HttpClient Http = new();
public static async Task<(string Date, decimal Close)> LastDailyCloseAsync(string symbol)
{
var url = $"https://query1.finance.yahoo.com/v8/financial-data-services/chart/{symbol}?interval=1d&range=5d";
var req = new HttpRequestMessage(HttpMethod.Get, url);
req.Headers.UserAgent.ParseAdd("Mozilla/5.0"); // Yahoo requires a User-Agent
var resp = await Http.SendAsync(req);
resp.EnsureSuccessStatusCode();
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStreamAsync());
var result = doc.RootElement.GetProperty("chart").GetProperty("result")[0];
var timestamps = result.GetProperty("timestamp");
var closes = result.GetProperty("indicators")
.GetProperty("quote")[0].GetProperty("close");
var i = timestamps.GetArrayLength() - 1;
var unix = timestamps[i].GetInt64();
var close = closes[i].GetDecimal(); // decimal — correct for a price
var date = DateTimeOffset.FromUnixTimeSeconds(unix).ToString("yyyy-MM-dd");
return (date, close);
}
}
class Program
{
static async Task Main()
{
var (date, close) = await YahooChart.LastDailyCloseAsync("AAPL");
Console.WriteLine($"AAPL close {date}: {close} USD");
}
}
This example is an honest look at the "scraping" route through Yahoo: no key needed, data close to real-time — but the endpoint is unofficial and can change without warning, so you can't build production on it. If you go this route at scale, plan for rotating proxies and rate limiting, because a single IP hammering the endpoint gets throttled fast.
6. What data type to store quotes in
The base rule is exactly the one from the currency-rate guide: prices and monetary values go in a decimal type only, never float/double. Binary floating point accumulates rounding error, and over a long backtest that produces discrepancies you can never reconcile.
| Field | Type | Why |
|---|---|---|
| Price (open/high/low/close, last) | NUMERIC(18,6) / Decimal / decimal |
lossless precision |
| Volume | BIGINT / int64 |
billions of shares won't fit an ordinary int |
| Adjusted close | NUMERIC(18,6) |
store it alongside the raw price, not instead of it |
| Bar timestamp | TIMESTAMPTZ |
must carry the exchange time zone |
| Instrument currency | CHAR(3) (ISO 4217) |
for portfolio conversion |
An example table for OHLCV candles:
CREATE TABLE quotes (
id BIGSERIAL PRIMARY KEY,
source VARCHAR(16) NOT NULL, -- 'NASDAQ', 'FINNHUB', 'AV', 'YAHOO'
exchange VARCHAR(16) NOT NULL, -- 'NASDAQ', 'NYSE', 'LSE'...
ticker VARCHAR(20) NOT NULL, -- AAPL, MSFT...
isin CHAR(12), -- reliable instrument identifier
ccy CHAR(3) NOT NULL, -- price currency: USD, GBP...
ts TIMESTAMPTZ NOT NULL, -- bar / quote timestamp
interval VARCHAR(8) NOT NULL DEFAULT '1d', -- 1m | 1h | 1d
open NUMERIC(18,6) NOT NULL,
high NUMERIC(18,6) NOT NULL,
low NUMERIC(18,6) NOT NULL,
close NUMERIC(18,6) NOT NULL,
adj_close NUMERIC(18,6), -- adjusted for splits/dividends
volume BIGINT NOT NULL DEFAULT 0,
fetched_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (source, exchange, ticker, interval, ts)
);
Separately, it's worth storing corporate actions (splits with their ratio and dividends with their date), because when they occur you have to recompute the adj_close history retroactively.
7. Practical tips
- Distinguish real-time, delayed and EOD. For most tasks (portfolio, analytics, a dashboard) delayed or end-of-day data is enough — it's cheaper and easier to license.
- Respect the licenses. This is the main difference from central-bank FX rates. Real-time market data is regulated (exchanges, FINRA, SEC), and even re-displaying it to your users can require an agreement. That's why, for example, Alpha Vantage's US real-time tier is paid. Check the source's terms before you publish anything.
- Build everything around a cache. With a 25-requests-a-day limit there's no other way: pull the history once, then do incremental updates and serve the rest from your own database.
- Treat splits like a denomination change. A sudden N-fold jump in price is almost always a corporate action, not a market move. A simple "change vs. previous day exceeds X%" check catches both splits and parsing errors.
- Identify the instrument by exchange + ticker (better still, by ISIN/FIGI). One ticker lives on several venues.
- Remember sessions and holidays. An empty response on a market holiday is normal; take the last available date from the response, not the date you requested.
- Don't build production on scraping Yahoo. Great for a prototype; for a service that has to run for years, use an API with guarantees. When scraping is the only option, expect to deal with CAPTCHAs and IP blocks.
8. Where this gets used
- Portfolio trackers — current asset value, P&L, conversion to a base currency.
- Algo trading and bots — signals and execution (this is where you actually need real-time and the order book).
- Dashboards and BI — market visualization, sector cuts.
- Screeners and backtests — filtering securities and testing strategies on history (OHLCV + adjusted).
- Robo-advisors and fintech — recommendations and automated management.
- Accounting and valuation — marking positions to market on a given date.
- Alerts — notifications when a price hits a set level.
Wrapping up
Scraping stock quotes is technically close to scraping FX rates — the same decimal types, the same caching, the same caution around "technical" jumps (only instead of a currency's denomination, here it's splits). But three things get added: the ticker is tied to an exchange (and better, to an ISIN/FIGI), the history needs adjusting for corporate actions, and — the most important thing in practice — the data is licensed: nearly everywhere only delayed and end-of-day are free, while real-time costs money and comes with strings attached. For most analytics and portfolio use cases a free aggregator plus a local cache is all you need, and it dovetails neatly with scraping exchange rates for multi-currency portfolios.
If you'd rather not babysit rate limits, corporate-action adjustments and licensing across a dozen sources, scraping.pro can build and run the pipeline for you as a data-as-a-service feed or a one-off custom data extraction job — clean OHLCV and fundamentals delivered on a schedule. Combine type discipline, a cache and respect for licenses, and you get quotes you can actually rely on in your calculations.