Это парный материал к статье «Парсинг курсов валют». Многое из неё переносится сюда почти без изменений — особенно дисциплина типов данных (никакого float для цен) и практики кэширования. Но у котировок акций есть своя специфика: тикеры и биржевые площадки, торговые сессии и праздники, корпоративные действия (сплиты и дивиденды), и — главное отличие от курсов центробанков — лицензирование данных. Биржевая информация в реальном времени регулируется биржами и регуляторами, и её, в отличие от свободно публикуемого официального курса ЦБ, далеко не всегда можно бесплатно получать и тем более перераспространять.
1. Что такое «котировка» и какие данные бывают
Под «котировкой» в зависимости от задачи понимают разные вещи, и это первое, что нужно определить:
- Last / текущая цена — цена последней сделки. То, что показывают в виджете «цена акции сейчас».
- OHLCV-бар (свеча) — Open, High, Low, Close и Volume за интервал (минута, час, день). Основа графиков и бэктестов.
- Bid/Ask (стакан) — лучшие цены покупки и продажи. Нужны для трейдинга, обычно это самые «дорогие» и лицензируемые данные.
- EOD (end-of-day) — итоговые цены закрытия дня. Дёшево/бесплатно, подходит для аналитики и портфельного учёта.
- Adjusted close — цена закрытия, скорректированная на сплиты и дивиденды, чтобы история была непрерывной.
И ключевая развилка по «свежести»: реальное время → задержка 15–20 минут → end-of-day. Чем свежее и детальнее данные, тем строже лицензия и выше цена.
2. Биржи СНГ и региональные источники
Главный «дружелюбный к разработчику» источник в регионе — MOEX ISS API. Это полноценный REST-сервис Московской биржи, который отдаёт акции, облигации, фьючерсы, индексы и валютные пары в JSON/XML/CSV. Данные с задержкой около 15 минут доступны бесплатно; реальное время и биржевой стакан — по подписке.
| Биржа | Что отдаёт | Доступ |
|---|---|---|
MOEX (Москва), iss.moex.com |
акции, облигации, фьючерсы, индексы, валютные пары | задержка ~15 мин бесплатно; реалтайм и стакан — по подписке |
KASE (Казахстан), kase.kz |
акции, индекс KASE, облигации | данные публикуются на сайте; стабильного публичного REST API уровня MOEX обычно нет — берут у вендоров или парсингом |
UZSE (Ташкент), uzse.uz |
акции узбекских эмитентов | данные на сайте |
| СПБ Биржа | иностранные бумаги | сайт/вендоры |
Удобный мостик к валютной статье: MOEX ISS отдаёт не только акции, но и валютные пары (например,
USD000UTSTOM— доллар к рублю на бирже). То есть одна интеграция с ISS закрывает сразу и акции, и биржевой курс валют — биржевой, а не официальный курс ЦБ.
Особенность формата ISS: данные приходят «колоночно» — отдельно массив имён колонок (columns) и массив строк (data). Чтобы достать нужное поле, надо сопоставить имя колонки с её индексом, а не полагаться на фиксированный порядок (см. пример на PHP).
3. Мировые API
Здесь свободного «как у ЦБ» источника нет: у всех есть ключ и лимиты, а реальное время почти всегда платное.
| Сервис | Покрытие | Ключ | Бесплатный лимит | Заметки |
|---|---|---|---|---|
| MOEX ISS | рынки MOEX | не нужен | задержка ~15 мин | реалтайм платно |
| Finnhub | акции US и интернац., FX, крипто | нужен | ~60 запросов/мин | есть WebSocket; история на free ограничена |
| Twelve Data | акции, FX, крипто | нужен | ~800 запросов/день | OHLC, индикаторы, чистый REST |
| Alpha Vantage | 200k+ тикеров, 20+ бирж | нужен | 25 запросов/день (5/мин) | EOD и индикаторы; реалтайм US — платно |
| yfinance (неоф. Yahoo) | очень широкое | не нужен | явного лимита нет, но это скрейпинг | для прототипов и обучения, не для продакшена |
| EODHD | 150k+ тикеров глобально | нужен | пробный | силён массовой выгрузкой истории |
| Financial Modeling Prep | цены + фундаментал | нужен | лимит | отчётность, мультипликаторы |
| Tiingo | EOD + фундаментал US | нужен | лимит | чистые end-of-day |
| Marketstack / Polygon.io | глобальные / реалтайм US | нужен | пробный / по сути платно | Polygon — низкая задержка, тики |
Важная свежая деталь: IEX Cloud закрылся 31 августа 2024 года. Если в каком-то гайде он ещё рекомендуется — это устаревшая информация; ближайшие замены — Alpha Vantage и Financial Modeling Prep.
Бесплатные тарифы отлично подходят для прототипа, но быстро упираются в лимиты (25 запросов в сутки у Alpha Vantage — это буквально пара десятков тикеров в день). Поэтому на бесплатном тарифе всегда строят логику вокруг кэша и локального хранения: тянут историю один раз, дальше только обновляют свежие точки.
4. Чем котировки акций сложнее курсов валют
Несколько отличий, которые ломают наивный парсер:
- Тикер не уникален. Один и тот же символ может торговаться на разных биржах. Поэтому ключ инструмента — это пара «биржа + тикер», а надёжнее — международные идентификаторы ISIN или FIGI.
- Торговые сессии и праздники. У каждой биржи свой график, часовой пояс, пред- и постторговые сессии. «Последняя цена» в выходные — это цена пятницы.
- Корпоративные действия. Сплит 1:10 «уронит» цену в 10 раз — но это не падение рынка, а технический пересчёт. Аналог поля
номинализ валютной статьи: проигнорируете — получите ложную аномалию. Дивиденды и сплиты учитывают через adjusted close. - Валюта инструмента. Цена бумаги выражена в валюте биржи (RUB, USD, KZT…), и для портфеля в одной валюте её нужно пересчитывать — вот тут и пригождаются курсы из соседней статьи.
- Объём (volume). Это целое число, но очень большое — миллионы и миллиарды штук. Под него нужен 64-битный целочисленный тип.
5. Пять решений на разных языках
Каждый пример бьёт в свой источник, чтобы показать разные форматы. Везде акцент на двух вещах из валютной статьи: цена хранится в десятичном типе (не float), объём — в целочисленном.
5.1. PHP — MOEX ISS (колоночный JSON, СНГ)
<?php
declare(strict_types=1);
/**
* Последняя цена акции на Московской бирже, основной режим TQBR.
* Бесплатно — с задержкой ~15 минут.
*/
function moexLastPrice(string $ticker, string $board = 'TQBR'): ?string
{
$url = sprintf(
'https://iss.moex.com/iss/engines/stock/markets/shares/boards/%s/securities/%s.json?iss.meta=off',
$board,
$ticker
);
$raw = file_get_contents($url);
if ($raw === false) {
throw new RuntimeException('Не удалось получить данные MOEX');
}
$json = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
// ISS отдаёт данные «колоночно»: отдельно columns, отдельно data
$columns = $json['marketdata']['columns'] ?? [];
$rows = $json['marketdata']['data'] ?? [];
if (!$rows) {
return null; // нет торгов или неверный тикер
}
$idx = array_flip($columns); // имя колонки -> индекс
$last = $rows[0][$idx['LAST']] ?? null; // цена последней сделки
return $last === null ? null : (string) $last;
}
echo 'SBER: ' . (moexLastPrice('SBER') ?? 'нет данных') . " RUB\n";
echo 'GAZP: ' . (moexLastPrice('GAZP') ?? 'нет данных') . " RUB\n";
Здесь показателен array_flip($columns) — сопоставление имени колонки с индексом. Это правильный способ читать ISS: порядок полей не гарантирован, и «магические» индексы вроде [12] ломаются при изменении ответа. Для бит-точной цены лучше запрашивать CSV или хранить сырой текст — JSON-декодер уже превращает число в float.
5.2. Python — Finnhub (текущая котировка, Decimal)
import os
import requests
from decimal import Decimal
FINNHUB_TOKEN = os.environ["FINNHUB_TOKEN"] # бесплатный ключ, ~60 запросов/мин
def finnhub_quote(symbol: str) -> dict[str, Decimal]:
"""Текущая котировка по символу (на бесплатном тарифе — рынок США)."""
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(...)) фиксирует ровно то значение, что пришло в JSON
return {
"current": Decimal(str(d["c"])), # текущая цена
"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 (откр. {q['open']}, макс. {q['high']})")
5.3. JavaScript / Node.js — Twelve Data (OHLC, объём)
// Бесплатно: ~800 запросов в день. Ключ обязателен.
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, // строки — не приводим к Number без необходимости
high: d.high,
low: d.low,
close: d.close,
volume: d.volume, // объём — большое целое, держим строкой/BigInt
exchange: d.exchange,
};
}
twelveQuote("MSFT").then((q) =>
console.log(`${q.symbol} (${q.exchange}): close ${q.close}, vol ${q.volume}`)
);
В JavaScript нет десятичного типа, а Number — это double. Поэтому цены оставлены строками; для арифметики над ценами берут decimal.js/big.js, а для объёма — нативный BigInt.
5.4. Go — Alpha Vantage (дневные свечи OHLCV)
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"sort"
"time"
)
// Alpha Vantage: бесплатно 25 запросов/сутки, 5/мин.
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)
}
// находим последнюю по дате свечу
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]
// цены оставлены строками; для расчётов — 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)
}
Удобно, что Alpha Vantage отдаёт цены строками — десятичная точность сохраняется «из коробки», достаточно не превращать их в float64.
5.5. C# / .NET — Yahoo Finance (неофициальный эндпоинт, decimal)
using System.Text.Json;
// ⚠️ Неофициальный эндпоинт Yahoo Finance — его же использует библиотека yfinance.
// Хорош для прототипов и обучения, но без гарантий и НЕ для продакшена.
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/finance/chart/{symbol}?interval=1d&range=5d";
var req = new HttpRequestMessage(HttpMethod.Get, url);
req.Headers.UserAgent.ParseAdd("Mozilla/5.0"); // Yahoo требует 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 — корректно для цены
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");
}
}
Этот пример честно показывает «скрейпинговый» маршрут через Yahoo: ключа не нужно, данные близки к реальному времени, но эндпоинт неофициальный и может смениться без предупреждения — на нём нельзя строить продакшен.
6. В каком типе данных хранить котировки
Базовое правило ровно то же, что в статье про курсы валют: цены и денежные величины — только в десятичном типе, никаких float/double. Двоичная плавающая точка накапливает ошибки округления, и на бэктестах за длинный период это даёт расхождения, которые невозможно свести.
| Поле | Тип | Почему |
|---|---|---|
| Цена (open/high/low/close, last) | NUMERIC(18,6) / Decimal / decimal |
точность без потерь |
| Объём (volume) | BIGINT / int64 |
миллиарды штук не влезают в обычный int |
| Adjusted close | NUMERIC(18,6) |
хранить рядом с «сырой» ценой, а не вместо |
| Время бара | TIMESTAMPTZ |
обязательно с часовым поясом биржи |
| Валюта инструмента | CHAR(3) (ISO 4217) |
для пересчёта портфеля |
Пример таблицы под свечи (OHLCV):
CREATE TABLE quotes (
id BIGSERIAL PRIMARY KEY,
source VARCHAR(16) NOT NULL, -- 'MOEX', 'FINNHUB', 'AV', 'YAHOO'
exchange VARCHAR(16) NOT NULL, -- биржа: 'MOEX', 'NASDAQ'...
ticker VARCHAR(20) NOT NULL, -- SBER, AAPL...
isin CHAR(12), -- надёжный идентификатор инструмента
ccy CHAR(3) NOT NULL, -- валюта цены: RUB, USD...
ts TIMESTAMPTZ NOT NULL, -- время бара/котировки
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), -- скорректировано на сплиты/дивиденды
volume BIGINT NOT NULL DEFAULT 0,
fetched_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (source, exchange, ticker, interval, ts)
);
Отдельно стоит хранить корпоративные действия (сплиты с коэффициентом и дивиденды с датой), потому что при их появлении историю adj_close приходится пересчитывать задним числом.
7. Практические советы
- Различайте реалтайм, задержку и EOD. Для большинства задач (портфель, аналитика, дашборд) хватает данных с задержкой или end-of-day — они дешевле и проще лицензируются.
- Уважайте лицензии. Это главное отличие от курсов ЦБ. Биржевые данные в реальном времени регулируются (биржи, FINRA, SEC), и даже их перепоказ пользователям может требовать соглашения. Поэтому, например, реалтайм по США у Alpha Vantage — платный. Прежде чем публиковать данные, проверьте условия источника.
- Стройте всё вокруг кэша. При лимите 25 запросов в сутки иначе никак: тянете историю один раз, дальше — только инкрементальные обновления, остальное отдаёте из своей базы.
- Учитывайте сплиты как «номинал». Резкий скачок цены в N раз — почти всегда корпоративное действие, а не движение рынка. Простая проверка «изменение к предыдущему дню больше X%» ловит и сплиты, и ошибки парсинга.
- Идентифицируйте инструмент по бирже + тикеру (а лучше по ISIN/FIGI). Один тикер живёт на разных площадках.
- Помните про сессии и праздники. Пустой ответ в выходной — норма; берите последнюю доступную дату из ответа, а не запрошенную.
- Не стройте продакшен на скрейпинге Yahoo. Для прототипа — отлично, для сервиса, который должен жить годами, — берите API с гарантиями.
8. Где это применяется
- Портфельные трекеры — текущая стоимость активов, P&L, пересчёт в базовую валюту.
- Алготрейдинг и торговые боты — сигналы и исполнение (тут уже нужен реалтайм и стакан).
- Дашборды и BI — визуализация рынка, отраслевые срезы.
- Скринеры и бэктест — фильтрация бумаг и проверка стратегий на истории (OHLCV + adjusted).
- Робо-эдвайзеры и финтех — рекомендации и автоматическое управление.
- Учёт и оценка — переоценка вложений по рыночной цене на дату.
- Алерты — уведомления о достижении ценой заданного уровня.
Итого
Парсинг котировок акций технически близок к парсингу курсов валют — те же десятичные типы, то же кэширование, та же осторожность с «техническими» скачками (только вместо номинала валюты здесь сплиты). Но добавляются три вещи: тикер привязан к бирже (и лучше к ISIN/FIGI), история требует корректировки на корпоративные действия, и — самое важное на практике — данные лицензируются: бесплатно почти везде доступны лишь задержка и end-of-day, а реальное время стоит денег и связано условиями. В регионе самый удобный публичный источник — MOEX ISS, который заодно отдаёт и биржевые валютные пары, смыкаясь с темой соседней статьи про курсы валют. Сложите дисциплину типов, кэш и уважение к лицензиям — и получите котировки, на которые можно опереться в расчётах.