Данные для бэктеста на Мосбирже: свой сборщик bid/ask на Python, архивы биржи и DMA

В БКС есть направление прямого доступа к бирже: мы подключаем алготрейдеров к шлюзам Московской биржи, помогаем с роботами, данными и инфраструктурой. Один из частых вопросов клиентов – где взять историю bid/ask для бэктеста. Этот текст – развернутый ответ: что есть в брокерском API, что продает биржа и когда нужен поток с биржи напрямую. С кодом сборщика и свечей. Статью подготовил Руслан Гнатовский, руководитель направления DMA.
Бэктест на минутных свечах отвечает на вопрос «была ли идея», но не на вопрос «заработал бы на ней робот». Свеча не знает, по какой цене на самом деле можно было войти, кто был инициатором сделок, сколько объема стояло на нужной цен и сколько заявок было перед нами.
Порядок цифр на фьючерсе на доллар (Si): шаг цены 1 пункт, это 1 рубль на контракт. Робот, который входит и выходит по рынку, покупает по ask, продает по bid и на каждом круге отдает спред. Спред 1 пункт, 100 кругов в день по 10 контрактов – 1 000 рублей в день, около 20 000 в месяц, которых нет в бэктесте по цене закрытия свечи.
Ниже три пути получить котировки: записать самому через брокерский API, купить архив у биржи и/или взять поток напрямую с биржи через DMA.
Какие данные нужны для какого бэктеста
Что проверяете | Каких данных достаточно |
|---|---|
Трендовую идею на дневках или часах | Свечи OHLCV |
Сколько съедают спред и проскальзывание при входе по рынку | Лучшие bid и ask на каждый момент |
Импульс, перевес агрессивных покупок над продажами | Сделки с направлением и объемом |
Исполнение лимитных заявок, влияние своего объема на цену | Стакан на несколько уровней |
Место в очереди, маркетмейкинг, высокочастотные стратегии | Журнал всех заявок: каждое выставление, снятие и сделка, стакан L3 |

Путь 1. Свой сборщик на Trade API
История в Trade API БКС есть только свечами: GET /api/v1/candles-chart, до 1 440 баров за запрос, длинный период – несколькими запросами. Сделки (/api/v1/last-trades) – только за текущую сессию, котировки и стакан – только текущие. Зато по WebSocket в реальном времени приходит все, из чего история складывается.
Адрес потока рыночных данных: wss://ws.broker.ru/trade-api-market-data-connector/api/v1/market-data/ws, авторизация – access-токен в заголовке Authorization: Bearer. Тип данных в подписке задает dataType:
| Поток | Что в сообщении |
|---|---|---|
0 | Стакан | до 20 уровней, глубина задается в |
1 | Свечи | текущая свеча, |
2 | Обезличенные сделки |
|
3 | Котировки |
|
Все dateTime – в UTC. Для фьючерсов отдельная подписка на рыночные данные не нужна, а тикер и код класса фьючерса берутся из справочника инструментов (метод инструментов по типу, тип FUTURES).
Токены
Refresh-токен выпускается бесплатно в веб-версии «БКС Мир инвестиций». Для записи достаточно типа «только для чтения». Refresh живет 90 суток, из него сборщик получает
access-токен на 24 часа: POST /trade-api-keycloak/realms/tradeapi/protocol/openid-connect/token, формаapplication/x-www-form-urlencoded с полями client_id=trade-api-read, grant_type=refresh_token и refresh_token.
Что учесть
Сутки жизни access-токена. Раз в сутки получаем новый и переподключаемся.
Ping каждые 30 секунд. Сервер закрывает соединение, если не получил pong. Библиотека
websocketsотвечает на pingсама, если не блокировать цикл чтения: никакой тяжелой обработки внутри него, только запись.Лимиты. На рыночные данные – до 20 соединений, до 10 сообщений в секунду от клиента и до 100 инструментов в одном соединении.
Время получения. Храним рядом с каждым сообщением: по нему видны дыры в записи.
Сборщик
import asyncio, json, os, time
import requests # pip install requests
import websockets # pip install websockets, Python 3.11+
TOKEN_URL = os.environ["BCS_TOKEN_URL"] # полный адрес метода из раздела «Авторизация»
REFRESH_TOKEN = os.environ["BCS_REFRESH_TOKEN"] # токен «только для чтения»
WS_URL = "wss://ws.broker.ru/trade-api-market-data-connector/api/v1/market-data/ws"
INSTRUMENTS = [{"ticker": "SBER", "classCode": "TQBR"}] # фьючерсы – из справочника, тип FUTURES
TRADES, QUOTES = 2, 3 # dataType: сделки и котировки
def get_access_token() -> str:
r = requests.post(TOKEN_URL, timeout=10, data={
"client_id": "trade-api-read",
"grant_type": "refresh_token",
"refresh_token": REFRESH_TOKEN,
})
r.raise_for_status()
return r.json()["access_token"]
async def record(path: str):
while True:
try:
headers = {"Authorization": f"Bearer {get_access_token()}"}
async with websockets.connect(WS_URL, additional_headers=headers) as ws:
for data_type in (QUOTES, TRADES):
await ws.send(json.dumps({"subscribeType": 0, "dataType": data_type,
"instruments": INSTRUMENTS}))
# до истечения суток жизни токена переподключаемся с новым
async with asyncio.timeout(20 * 3600):
with open(path, "a", encoding="utf-8") as f:
async for msg in ws:
f.write(json.dumps({"recv": time.time(), "msg": json.loads(msg)},
ensure_ascii=False) + "\n")
except Exception as e:
print("переподключение:", repr(e))
await asyncio.sleep(5)
asyncio.run(record("md.jsonl"))
Свечи из записи тиковых данных
Что такое тиковые данные: каждая сделка и каждое обновление котировки. Из записи таких данных собираются бары любого периода. Ниже бары в двух вариантах сразу: по сделкам, с объемом в контрактах и перевесом агрессора, и по котировкам, со спредом внутри бара. Период задает TF.
import json
import pandas as pd
TF = "5min" # "1min", "10s" – любой период
TICKER = "SBER"
rows = [json.loads(line)["msg"] for line in open("md.jsonl", encoding="utf-8")]
trades = pd.DataFrame([r for r in rows if r.get("responseType") == "LastTrades" and "price" in r])
quotes = pd.DataFrame([r for r in rows if r.get("responseType") == "Quotes" and "bid" in r])
for df in (trades, quotes):
df["dateTime"] = pd.to_datetime(df["dateTime"])
t = trades[trades.ticker == TICKER].set_index("dateTime")
q = quotes[quotes.ticker == TICKER].set_index("dateTime")
# по сделкам: цены, объем в контрактах и в деньгах, агрессивные покупки и продажи
bars = t["price"].resample(TF).ohlc()
bars["contracts"] = t["quantity"].resample(TF).sum()
bars["turnover"] = t["volume"].resample(TF).sum()
bars["buy"] = t.loc[t.side == "BUY", "quantity"].resample(TF).sum()
bars["sell"] = t.loc[t.side == "SELL", "quantity"].resample(TF).sum()
# по котировкам: bid и ask на закрытии бара, спред внутри бара
# (среднее по обновлениям котировки, а не по времени)
spread = q["offer"] - q["bid"]
bars["bid_close"] = q["bid"].resample(TF).last()
bars["ask_close"] = q["offer"].resample(TF).last()
bars["spread_mean"] = spread.resample(TF).mean()
bars["spread_max"] = spread.resample(TF).max()
buy - sell за бар – признак перевеса агрессивных покупателей, которого в обычных свечах нет.
Полна ли запись
Число записанных за день сделок по инструменту сравниваем с NUMTRADES в итогах дня на ISS, для фьючерса Si: https://iss.moex.com/iss/history/engines/futures/markets/forts/securities/SiZ6.json. Совпадает – сделки записаны все.
Сколько это весит
По Si с 19 по 26 октября проходило от 150 до 240 тысяч сделок в день. Строка с одной сделкой в формате выше – около 216 байт, с котировкой – около 409. Только сделки по одному инструменту – 30–50 МБ в день, котировки добавят еще. JSON сжимается в разы, а для анализа его удобно раз в сутки перекладывать в Parquet.
Длинной истории запись сразу не даст: пары недель хватит, чтобы оценить спред, проскальзывание и исполнение, но не разные режимы рынка. Для них – второй путь.
Путь 2. Архив биржи
Московская биржа продает историю: moex.com/ru/orders?historicaldata - раздел «Реестры заявок и сделок». Данные обезличенные, минимальный период – календарный месяц, есть бесплатные пробные данные.
Тип | Что внутри | Срочный рынок | Акции | Валютный рынок |
|---|---|---|---|---|
A – Full Orderbook | Все сделки и все заявки | с января 2016 | с апреля 2016 | с января 2006 |
B – Top of Book | Все сделки и лучшие заявки | с января 2016 | с апреля 2016 | с января 2006 |
C | Только сделки | с ноября 2000 | с января 2000 | с января 2000 |
Цены на 27 октября 2026 года, без НДС:
Тип | Инструмент, месяц | Рынок, месяц | Инструмент, год | Рынок, год |
|---|---|---|---|---|
A | 4 500 руб | 15 000 руб | 45 000 руб | 150 000 руб |
B | 1 500 руб | 4 500 руб | 15 000 руб | 45 000 руб |
C | – | 3 750 руб | – | – |
Для бэктеста по bid и ask хватает типа B. Из типа A стакан восстанавливается на любой момент и на любую глубину.

Путь 3. Поток с биржи через DMA
Брокерский API – это поток через инфраструктуру брокера со стаканом до 20 уровней. Когда важны очередь заявок и задержки, данные берут у биржи через шлюзы прямого доступа:
Сервис | Что дает | Тариф биржи, без НДС |
|---|---|---|
Plaza II, срочный рынок | Опция «полный журнал заявок» – обезличенный журнал всех заявок торговой сессии. Шлюз в офисе или в колокации биржи | 8 000 руб в месяц к логину Plaza II |
FAST | Фондовый, срочный и валютный рынки. На срочном – опция «полный журнал заявок» (Full_orders_log), стакан любой глубины | 21 000 руб в месяц за опцию |
SIMBA | Самый быстрый поток биржи, полный журнал заявок, задержка ниже, чем у FAST. Зона колокации | 30 000 руб в месяц и 30 000 разово за рынок |
Запись такого потока у себя – самая подробная история: каждая заявка и биржевые метки времени. К тарифам биржи добавляются логин и подключение, полную стоимость под стратегию посчитает команда DMA БКС.
Как выбрать
Ситуация | Путь |
|---|---|
Нужно понять реальный спред и исполнение на своих инструментах | Свой сборщик на Trade API |
Нужно проверить идею на годах истории | Архив биржи: тип B для спреда, тип A для стакана |
Стратегия чувствительна к задержкам, нужны очередь заявок, полный стакан и точные биржевые метки времени | DMA: Plaza II, FAST или SIMBA и запись у себя |
Пути часто совмещают: прошлые годы – из архива биржи, дальше – своя запись.
Ссылки
Документация Trade API БКС: trade-api.bcs.ru, ограничения – trade-api.bcs.ru/restrictions.
Архив биржи и пробные данные: moex.com/ru/orders?historicaldata.
ISS, итоги дня по Si: iss.moex.com/iss/history/engines/futures/markets/forts/securities/SiZ6.
Тарифы биржи на прямой доступ: moex.com/s324, SIMBA – moex.com/s3320.
Вопросы по сбору данных и подключению своей системы: dma@bcs.ru.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.