Неофициальная библиотека для типизированной выгрузки данных из ISS API Московской биржи.
- Публичный слой работает со строгими доменными типами (
SecId,BoardId,EngineName,MarketName,IndexId). - Внешние данные ISS валидируются на границе и преобразуются в модели через
TryFrom/try_new. - Для пост-обработки коллекций используется fluent-стиль через extension-traits (
moex_client::prelude::*). - Для непокрытых endpoint-ов есть raw escape hatch с типизированными
iss.*опциями.
Источник перечня: https://iss.moex.com/iss/reference/
| Возможность выгрузки | Эндпоинт из ISS reference | Реализовано |
|---|---|---|
| Список торговых систем | /iss/engines |
[x] |
| Список рынков торговой системы | /iss/engines/[engine]/markets |
[x] |
| Справочник режимов торгов | /iss/engines/[engine]/markets/[market]/boards |
[x] |
| Инструменты по режиму торгов | /iss/engines/[engine]/markets/[market]/boards/[board]/securities |
[x] |
| Стакан по инструменту и режиму | /iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/orderbook |
[x] |
| Сделки по инструменту и режиму | /iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/trades |
[x] |
| Свечи по инструменту и режиму | /iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/candles |
[x] |
| Список индексов фондового рынка | /iss/statistics/engines/stock/markets/index/analytics |
[x] |
| Аналитика состава индекса | /iss/statistics/engines/stock/markets/index/analytics/[indexid] |
[x] |
| Общий список бумаг MOEX | /iss/securities |
[x] |
Карточка бумаги по secid |
/iss/securities/[security] |
[x] |
Инструменты на уровне рынка (без boards/[board]) |
/iss/engines/[engine]/markets/[market]/securities |
[x] |
Сделки/стаканы на уровне рынка (без boards/[board]) |
/iss/engines/[engine]/markets/[market]/trades, /orderbook |
[x] |
| Границы свечей по инструменту | /iss/engines/[engine]/markets/[market]/securities/[security]/candleborders |
[x] |
| История: доступные даты по инструменту | /iss/history/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/dates |
[x] |
| Исторические выгрузки | /iss/history/... |
[x] |
| Обороты и secstats | /iss/turnovers, /iss/engines/[engine]/turnovers, /iss/engines/[engine]/markets/[market]/secstats |
[x] |
| ReferenceData 2.0 | /iss/referencedata/... |
[ ] |
| Новости и события | /iss/sitenews, /iss/events |
[x] |
По умолчанию включены: blocking, rustls-tls.
-
async— асинхронный клиент. -
blocking— блокирующий клиент. -
history— эндпоинты/history/.... -
news— эндпоинты/sitenewsи/events. -
rustls-tls/native-tls— выбор TLS backend дляreqwest. -
Только async API:
moex-client = { version = "...", default-features = false, features = ["async", "rustls-tls"] } -
Только blocking API:
moex-client = { version = "...", default-features = false, features = ["blocking", "rustls-tls"] }
use moex_client::blocking::Client;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder()
.user_agent_from_crate()
.metadata(false)
.build()?;
let engines = client.engines()?;
println!("engines: {}", engines.len());
Ok(())
}Асинхронный вариант:
use moex_client::r#async::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder()
.user_agent_from_crate()
.metadata(false)
.build()?;
let engines = client.engines().await?;
println!("engines: {}", engines.len());
let indexid = moex_client::models::IndexId::try_from("IMOEX")?;
let mut pages = client
.index(indexid)?
.analytics_pages(std::num::NonZeroU32::new(5000).expect("non-zero"));
while let Some(_page) = pages.next_page().await? {
// incremental processing
}
Ok(())
}Можно добавить proxy-правила через builder:
use moex_client::blocking::Client;
let proxy = reqwest::Proxy::all("http://127.0.0.1:3128")?;
let _client = Client::builder().proxy(proxy).build()?;Если нужно отключить proxy из окружения и системных настроек:
use moex_client::blocking::Client;
let _client = Client::builder().no_proxy().build()?;Для асинхронного клиента API аналогичный (moex_client::r#async::Client::builder()).
Для транспортного ретрая доступен helper без привязки к конкретным endpoint-ам:
use std::num::NonZeroU32;
use std::time::Duration;
use moex_client::blocking::Client;
use moex_client::{RetryPolicy, with_retry};
fn fetch_with_retry(client: &Client) -> Result<(), moex_client::MoexError> {
let policy = RetryPolicy::new(NonZeroU32::new(3).expect("non-zero"))
.with_delay(Duration::from_millis(400));
let _engines = with_retry(policy, || client.engines())?;
Ok(())
}Асинхронный вариант использует with_retry_async(...) и принимает sleep-функцию
из runtime приложения (например, tokio::time::sleep).
use std::time::Duration;
use moex_client::blocking::Client;
use moex_client::{RateLimit, RateLimiter, with_rate_limit};
fn fetch_with_rate_limit(client: &Client) -> Result<(), moex_client::MoexError> {
let mut limiter = RateLimiter::new(RateLimit::every(Duration::from_millis(250)));
let _engines = with_rate_limit(&mut limiter, || client.engines())?;
Ok(())
}Асинхронный вариант использует with_rate_limit_async(...) и, как with_retry_async(...),
принимает функцию sleep из runtime приложения.
Для blocking-клиента лимит можно зафиксировать в builder-е и применять автоматически ко всем запросам:
use std::time::Duration;
use moex_client::blocking::Client;
use moex_client::RateLimit;
let _client = Client::builder()
.rate_limit(RateLimit::every(Duration::from_millis(250)))
.build()?;Для async-клиента вместе с лимитом нужно задать функцию ожидания из вашего runtime:
use std::time::Duration;
use moex_client::r#async::Client;
use moex_client::RateLimit;
let _client = Client::builder()
.rate_limit(RateLimit::every(Duration::from_millis(250)))
.rate_limit_sleep(tokio::time::sleep)
.build()?;Слой Client остаётся endpoint-level, а бизнес-выборка делается через extension-traits:
IndexesExt, IndexAnalyticsExt, SecurityBoardsExt.
Для подключения fluent-методов достаточно одного импорта:
use moex_client::prelude::*;Пример:
use std::num::NonZeroU32;
use moex_client::blocking::Client;
use moex_client::models::{PageRequest, SecId};
use moex_client::prelude::*;
fn demo(client: &Client, indexid: &moex_client::models::IndexId, secid: &SecId) -> Result<(), moex_client::MoexError> {
let _indexes = client.indexes()?.into_actual_by_till();
let _components = client
.index(indexid.clone())?
.analytics(PageRequest::all(NonZeroU32::new(5000).expect("non-zero limit")))?
.into_actual_by_session()
.into_sorted_by_weight_desc();
let _board = client
.security(secid.clone())?
.boards()?
.into_stock_primary_or_first();
let _security_info = client.security(secid.clone())?.info()?;
Ok(())
}Для endpoint-ов, которые ещё не покрыты строгими методами, доступен low-level builder:
use moex_client::blocking::Client;
use serde_json::Value;
fn demo_raw(client: &Client) -> Result<Value, moex_client::MoexError> {
client
.raw()
.path("history/engines/stock/markets/shares/securities")
.param("date", "2026-03-06")
.only("history")
.columns("history", "SECID,BOARDID,CLOSE")
.send_json::<Value>()
}Если не хочется собирать path строкой, можно использовать типизированный endpoint:
use moex_client::blocking::Client;
use moex_client::{IssEndpoint, MoexError};
use moex_client::models::{BoardId, EngineName, MarketName};
fn demo_typed_raw(client: &Client) -> Result<String, MoexError> {
let engine = EngineName::try_from("stock")?;
let market = MarketName::try_from("shares")?;
let board = BoardId::try_from("TQBR")?;
client
.raw_endpoint(IssEndpoint::Securities {
engine: &engine,
market: &market,
board: &board,
})
.columns("securities", "SECID,SHORTNAME")
.send_payload()
}path(...) принимает формы вида engines, engines.json, /iss/engines, /iss/engines.json.
Путь нормализуется к относительному <endpoint>.json; query-строку в path передавать нельзя, для этого используйте .param(...).
Системные параметры iss.* можно задавать через типизированные опции, а при необходимости
получать полный HTTP-ответ (status/headers/body):
use moex_client::blocking::Client;
use moex_client::{IssRequestOptions, IssToggle};
fn demo_raw_response(client: &Client) -> Result<u16, moex_client::MoexError> {
let response = client
.raw()
.path("engines")
.options(
IssRequestOptions::new()
.metadata(IssToggle::Off)
.data(IssToggle::On)
.version(IssToggle::On)
.json("extended"),
)
.send_response()?;
Ok(response.status().as_u16())
}Если нужен не весь JSON, а только строки конкретной таблицы, можно декодировать их сразу в свой тип:
use moex_client::blocking::Client;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct HistoryCloseRow {
#[serde(rename = "SECID")]
secid: String,
#[serde(rename = "BOARDID")]
boardid: String,
#[serde(rename = "CLOSE")]
close: Option<f64>,
}
fn demo_raw_table(client: &Client) -> Result<Vec<HistoryCloseRow>, moex_client::MoexError> {
client
.raw()
.path("history/engines/stock/markets/shares/securities")
.param("date", "2026-03-06")
.only("history")
.columns("history", "SECID,BOARDID,CLOSE")
.send_table("history")
}Для парсинга payload-ов без клиента есть отдельный модуль moex_client::decode:
fn parse_indexes(payload: &str) -> Result<Vec<moex_client::models::Index>, moex_client::MoexError> {
moex_client::decode::indexes_json(payload)
}Также доступен generic-декодер выбранной таблицы в пользовательский тип:
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct CustomRow {
#[serde(rename = "SECID")]
secid: String,
}
fn parse_custom_table(payload: &str) -> Result<Vec<CustomRow>, moex_client::MoexError> {
moex_client::decode::raw_table_rows_json(payload, "custom/endpoint.json", "securities")
}Если из одного payload нужно извлечь несколько таблиц, можно один раз разобрать top-level блоки:
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct SecurityRow {
#[serde(rename = "SECID")]
secid: String,
}
#[derive(Debug, Deserialize)]
struct MarketDataRow {
#[serde(rename = "SECID")]
secid: String,
}
fn parse_two_tables(payload: &str) -> Result<(Vec<SecurityRow>, Vec<MarketDataRow>), moex_client::MoexError> {
let mut tables = moex_client::decode::raw_tables_json(payload, "custom/endpoint.json")?;
let securities = tables.take_rows("securities")?;
let marketdata = tables.take_rows("marketdata")?;
Ok((securities, marketdata))
}take_rows(...) забирает таблицу из внутреннего кэша RawTables.
Если нужен borrowed-доступ к ячейкам без DeserializeOwned, используйте raw_table_view_json:
fn parse_borrowed(payload: &str) -> Result<(), moex_client::MoexError> {
let table = moex_client::decode::raw_table_view_json(payload, "custom/endpoint.json", "securities")?;
let secid: &str = table
.deserialize_value(0, "SECID")?
.unwrap_or_default();
assert!(!secid.is_empty());
Ok(())
}Для endpoint-ов index_analytics/securities/trades/candles доступен единый режим страниц через PageRequest
и fluent scope с фиксацией engine/market/board/security.
Также есть ленивые paginator-объекты с next_page()/all():
use std::num::NonZeroU32;
use moex_client::blocking::Client;
use moex_client::models::{BoardId, CandleQuery, EngineName, MarketName, PageRequest, SecId};
fn demo_scoped(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
let engine = EngineName::try_from("stock")?;
let market = MarketName::try_from("shares")?;
let board = BoardId::try_from("TQBR")?;
let secid = SecId::try_from("SBER")?;
let _global_securities = client.global_securities_query(PageRequest::first_page())?;
let market_scope = client
.engine(engine.clone())?
.market(market.clone())?;
let _market_securities = market_scope
.securities(PageRequest::first_page())?;
let _market_trades = market_scope
.trades(PageRequest::first_page())?;
let _turnovers = client.turnovers()?;
let _engine_turnovers = client.engine_turnovers(&engine)?;
let _secstats = market_scope.secstats(PageRequest::first_page())?;
let _sitenews = client.sitenews_query(PageRequest::first_page())?;
let _events = client.events_query(PageRequest::first_page())?;
let _candle_borders = market_scope.candle_borders(&secid)?;
let security_scope = market_scope
.board(board.clone())?
.security(secid.clone())?;
let _history_dates = security_scope.history_dates()?;
let _history = security_scope.history(PageRequest::first_page())?;
let _trades = security_scope
.trades(PageRequest::all(NonZeroU32::new(1000).expect("non-zero")))?;
let _candles = security_scope
.candles(CandleQuery::default(), PageRequest::first_page())?;
let _all_trades = security_scope
.trades_pages(NonZeroU32::new(1000).expect("non-zero"))
.all()?;
let mut pages = client
.index(moex_client::models::IndexId::try_from("IMOEX")?)?
.analytics_pages(NonZeroU32::new(5000).expect("non-zero"));
while let Some(_page) = pages.next_page()? {
// incremental processing
}
Ok(())
}cargo run --example actual_indexes_dump