Skip to main content

moex_client/moex/
client.rs

1use std::num::NonZeroU32;
2#[cfg(any(feature = "blocking", feature = "async"))]
3use std::sync::Mutex;
4use std::time::Duration;
5
6#[cfg(feature = "blocking")]
7use reqwest::blocking::{Client, ClientBuilder};
8use reqwest::{Url, header::HeaderMap};
9
10use crate::models::{
11    Board, BoardId, Candle, CandleBorder, CandleQuery, Engine, EngineName, Index, IndexAnalytics,
12    IndexId, Market, MarketName, OrderbookLevel, PageRequest, Pagination, ParseBoardIdError,
13    ParseEngineNameError, ParseIndexError, ParseMarketNameError, ParseSecIdError, SecId, SecStat,
14    Security, SecurityBoard, SecuritySnapshot, Trade, Turnover,
15};
16#[cfg(feature = "news")]
17use crate::models::{Event, SiteNews};
18#[cfg(feature = "history")]
19use crate::models::{HistoryDates, HistoryRecord};
20
21use super::constants::*;
22use super::payload::{
23    decode_board_security_snapshots_json_with_endpoint, decode_boards_json_with_endpoint,
24    decode_candle_borders_json_with_endpoint, decode_candles_json_with_endpoint,
25    decode_engines_json_payload, decode_index_analytics_json_with_endpoint,
26    decode_indexes_json_payload, decode_markets_json_with_endpoint,
27    decode_orderbook_json_with_endpoint, decode_raw_table_rows_json_with_endpoint,
28    decode_secstats_json_with_endpoint, decode_securities_json_with_endpoint,
29    decode_security_boards_json_with_endpoint, decode_trades_json_with_endpoint,
30    decode_turnovers_json_with_endpoint,
31};
32#[cfg(feature = "news")]
33use super::payload::{decode_events_json_with_endpoint, decode_sitenews_json_with_endpoint};
34#[cfg(feature = "history")]
35use super::payload::{decode_history_dates_json_with_endpoint, decode_history_json_with_endpoint};
36use super::{
37    IssEndpoint, IssRequestOptions, IssToggle, MoexError, RawIssResponse, RepeatPagePolicy,
38};
39#[cfg(any(feature = "blocking", feature = "async"))]
40use super::{RateLimit, RateLimiter};
41
42/// Блокирующий клиент ISS API Московской биржи.
43///
44/// Клиент хранит базовый URL, режим выдачи `iss.meta` и переиспользуемый
45/// экземпляр `reqwest::blocking::Client`.
46#[cfg(feature = "blocking")]
47pub struct BlockingMoexClient {
48    base_url: Url,
49    metadata: bool,
50    client: Client,
51    rate_limiter: Option<Mutex<RateLimiter>>,
52}
53
54/// Builder для конфигурации [`BlockingMoexClient`].
55#[cfg(feature = "blocking")]
56pub struct BlockingMoexClientBuilder {
57    base_url: Option<Url>,
58    metadata: bool,
59    client: Option<Client>,
60    http_client: ClientBuilder,
61    rate_limit: Option<RateLimit>,
62}
63
64/// Асинхронный клиент ISS API Московской биржи.
65///
66/// Клиент хранит базовый URL, режим выдачи `iss.meta` и переиспользуемый
67/// экземпляр `reqwest::Client`.
68#[cfg(feature = "async")]
69pub struct AsyncMoexClient {
70    base_url: Url,
71    metadata: bool,
72    client: reqwest::Client,
73    rate_limit: Option<AsyncRateLimitState>,
74}
75
76/// Builder для конфигурации [`AsyncMoexClient`].
77#[cfg(feature = "async")]
78pub struct AsyncMoexClientBuilder {
79    base_url: Option<Url>,
80    metadata: bool,
81    client: Option<reqwest::Client>,
82    http_client: reqwest::ClientBuilder,
83    rate_limit: Option<RateLimit>,
84    rate_limit_sleep: Option<AsyncRateLimitSleep>,
85}
86
87#[cfg(feature = "async")]
88type AsyncSleepFuture = std::pin::Pin<Box<dyn std::future::Future<Output = ()> + 'static>>;
89
90#[cfg(feature = "async")]
91type AsyncRateLimitSleep = std::sync::Arc<dyn Fn(Duration) -> AsyncSleepFuture + Send + Sync>;
92
93#[cfg(feature = "async")]
94struct AsyncRateLimitState {
95    limiter: Mutex<RateLimiter>,
96    sleep: AsyncRateLimitSleep,
97}
98
99/// Универсальный builder для произвольных ISS endpoint-ов.
100///
101/// Служит low-level escape hatch для endpoint-ов, которые не покрыты
102/// строгим high-level API.
103#[cfg(feature = "blocking")]
104pub struct RawIssRequestBuilder<'a> {
105    client: &'a BlockingMoexClient,
106    path: Option<Box<str>>,
107    query: Vec<(Box<str>, Box<str>)>,
108}
109
110/// Асинхронный универсальный builder для произвольных ISS endpoint-ов.
111#[cfg(feature = "async")]
112pub struct AsyncRawIssRequestBuilder<'a> {
113    client: &'a AsyncMoexClient,
114    path: Option<Box<str>>,
115    query: Vec<(Box<str>, Box<str>)>,
116}
117
118/// Асинхронный ленивый paginator по страницам `index_analytics`.
119#[cfg(feature = "async")]
120pub struct AsyncIndexAnalyticsPages<'a> {
121    client: &'a AsyncMoexClient,
122    indexid: &'a IndexId,
123    pagination: PaginationTracker<(chrono::NaiveDate, SecId)>,
124}
125
126/// Асинхронный ленивый paginator по страницам `securities`.
127#[cfg(feature = "async")]
128pub struct AsyncSecuritiesPages<'a> {
129    client: &'a AsyncMoexClient,
130    engine: &'a EngineName,
131    market: &'a MarketName,
132    board: &'a BoardId,
133    pagination: PaginationTracker<SecId>,
134}
135
136/// Асинхронный ленивый paginator по страницам глобального `securities`.
137#[cfg(feature = "async")]
138pub struct AsyncGlobalSecuritiesPages<'a> {
139    client: &'a AsyncMoexClient,
140    pagination: PaginationTracker<SecId>,
141}
142
143/// Асинхронный ленивый paginator по страницам `sitenews`.
144#[cfg(all(feature = "async", feature = "news"))]
145pub struct AsyncSiteNewsPages<'a> {
146    client: &'a AsyncMoexClient,
147    pagination: PaginationTracker<u64>,
148}
149
150/// Асинхронный ленивый paginator по страницам `events`.
151#[cfg(all(feature = "async", feature = "news"))]
152pub struct AsyncEventsPages<'a> {
153    client: &'a AsyncMoexClient,
154    pagination: PaginationTracker<u64>,
155}
156
157/// Асинхронный ленивый paginator по страницам market-level `securities`.
158#[cfg(feature = "async")]
159pub struct AsyncMarketSecuritiesPages<'a> {
160    client: &'a AsyncMoexClient,
161    engine: &'a EngineName,
162    market: &'a MarketName,
163    pagination: PaginationTracker<SecId>,
164}
165
166/// Асинхронный ленивый paginator по страницам market-level `trades`.
167#[cfg(feature = "async")]
168pub struct AsyncMarketTradesPages<'a> {
169    client: &'a AsyncMoexClient,
170    engine: &'a EngineName,
171    market: &'a MarketName,
172    pagination: PaginationTracker<u64>,
173}
174
175/// Асинхронный ленивый paginator по страницам `trades`.
176#[cfg(feature = "async")]
177pub struct AsyncTradesPages<'a> {
178    client: &'a AsyncMoexClient,
179    engine: &'a EngineName,
180    market: &'a MarketName,
181    board: &'a BoardId,
182    security: &'a SecId,
183    pagination: PaginationTracker<u64>,
184}
185
186/// Асинхронный ленивый paginator по страницам `history`.
187#[cfg(all(feature = "async", feature = "history"))]
188pub struct AsyncHistoryPages<'a> {
189    client: &'a AsyncMoexClient,
190    engine: &'a EngineName,
191    market: &'a MarketName,
192    board: &'a BoardId,
193    security: &'a SecId,
194    pagination: PaginationTracker<chrono::NaiveDate>,
195}
196
197/// Асинхронный ленивый paginator по страницам `secstats`.
198#[cfg(feature = "async")]
199pub struct AsyncSecStatsPages<'a> {
200    client: &'a AsyncMoexClient,
201    engine: &'a EngineName,
202    market: &'a MarketName,
203    pagination: PaginationTracker<(SecId, BoardId)>,
204}
205
206/// Асинхронный ленивый paginator по страницам `candles`.
207#[cfg(feature = "async")]
208pub struct AsyncCandlesPages<'a> {
209    client: &'a AsyncMoexClient,
210    engine: &'a EngineName,
211    market: &'a MarketName,
212    board: &'a BoardId,
213    security: &'a SecId,
214    query: CandleQuery,
215    pagination: PaginationTracker<chrono::NaiveDateTime>,
216}
217
218/// Ленивый paginator по страницам `index_analytics`.
219#[cfg(feature = "blocking")]
220pub struct IndexAnalyticsPages<'a> {
221    client: &'a BlockingMoexClient,
222    indexid: &'a IndexId,
223    pagination: PaginationTracker<(chrono::NaiveDate, SecId)>,
224}
225
226/// Ленивый paginator по страницам `securities`.
227#[cfg(feature = "blocking")]
228pub struct SecuritiesPages<'a> {
229    client: &'a BlockingMoexClient,
230    engine: &'a EngineName,
231    market: &'a MarketName,
232    board: &'a BoardId,
233    pagination: PaginationTracker<SecId>,
234}
235
236/// Ленивый paginator по страницам глобального `securities`.
237#[cfg(feature = "blocking")]
238pub struct GlobalSecuritiesPages<'a> {
239    client: &'a BlockingMoexClient,
240    pagination: PaginationTracker<SecId>,
241}
242
243/// Ленивый paginator по страницам `sitenews`.
244#[cfg(all(feature = "blocking", feature = "news"))]
245pub struct SiteNewsPages<'a> {
246    client: &'a BlockingMoexClient,
247    pagination: PaginationTracker<u64>,
248}
249
250/// Ленивый paginator по страницам `events`.
251#[cfg(all(feature = "blocking", feature = "news"))]
252pub struct EventsPages<'a> {
253    client: &'a BlockingMoexClient,
254    pagination: PaginationTracker<u64>,
255}
256
257/// Ленивый paginator по страницам market-level `securities`.
258#[cfg(feature = "blocking")]
259pub struct MarketSecuritiesPages<'a> {
260    client: &'a BlockingMoexClient,
261    engine: &'a EngineName,
262    market: &'a MarketName,
263    pagination: PaginationTracker<SecId>,
264}
265
266/// Ленивый paginator по страницам market-level `trades`.
267#[cfg(feature = "blocking")]
268pub struct MarketTradesPages<'a> {
269    client: &'a BlockingMoexClient,
270    engine: &'a EngineName,
271    market: &'a MarketName,
272    pagination: PaginationTracker<u64>,
273}
274
275/// Ленивый paginator по страницам `trades`.
276#[cfg(feature = "blocking")]
277pub struct TradesPages<'a> {
278    client: &'a BlockingMoexClient,
279    engine: &'a EngineName,
280    market: &'a MarketName,
281    board: &'a BoardId,
282    security: &'a SecId,
283    pagination: PaginationTracker<u64>,
284}
285
286/// Ленивый paginator по страницам `history`.
287#[cfg(all(feature = "blocking", feature = "history"))]
288pub struct HistoryPages<'a> {
289    client: &'a BlockingMoexClient,
290    engine: &'a EngineName,
291    market: &'a MarketName,
292    board: &'a BoardId,
293    security: &'a SecId,
294    pagination: PaginationTracker<chrono::NaiveDate>,
295}
296
297/// Ленивый paginator по страницам `secstats`.
298#[cfg(feature = "blocking")]
299pub struct SecStatsPages<'a> {
300    client: &'a BlockingMoexClient,
301    engine: &'a EngineName,
302    market: &'a MarketName,
303    pagination: PaginationTracker<(SecId, BoardId)>,
304}
305
306/// Ленивый paginator по страницам `candles`.
307#[cfg(feature = "blocking")]
308pub struct CandlesPages<'a> {
309    client: &'a BlockingMoexClient,
310    engine: &'a EngineName,
311    market: &'a MarketName,
312    board: &'a BoardId,
313    security: &'a SecId,
314    query: CandleQuery,
315    pagination: PaginationTracker<chrono::NaiveDateTime>,
316}
317
318#[derive(Clone)]
319/// Асинхронный scope по `indexid`, владеющий значением.
320///
321/// Полезен для ergonomic-chain, где вход передаётся как `impl TryInto<IndexId>`.
322#[cfg(feature = "async")]
323pub struct AsyncOwnedIndexScope<'a> {
324    client: &'a AsyncMoexClient,
325    indexid: IndexId,
326}
327
328#[derive(Clone)]
329/// Асинхронный scope по `engine`, владеющий значением.
330#[cfg(feature = "async")]
331pub struct AsyncOwnedEngineScope<'a> {
332    client: &'a AsyncMoexClient,
333    engine: EngineName,
334}
335
336#[derive(Clone)]
337/// Асинхронный scope по `engine/market`, владеющий значениями.
338#[cfg(feature = "async")]
339pub struct AsyncOwnedMarketScope<'a> {
340    client: &'a AsyncMoexClient,
341    engine: EngineName,
342    market: MarketName,
343}
344
345#[derive(Clone)]
346/// Асинхронный scope по `engine/market/security`, владеющий значениями.
347#[cfg(feature = "async")]
348pub struct AsyncOwnedMarketSecurityScope<'a> {
349    client: &'a AsyncMoexClient,
350    engine: EngineName,
351    market: MarketName,
352    security: SecId,
353}
354
355#[derive(Clone)]
356/// Асинхронный scope по `engine/market/board`, владеющий значениями.
357#[cfg(feature = "async")]
358pub struct AsyncOwnedBoardScope<'a> {
359    client: &'a AsyncMoexClient,
360    engine: EngineName,
361    market: MarketName,
362    board: BoardId,
363}
364
365#[derive(Clone)]
366/// Асинхронный scope по `securities/{secid}`, владеющий `secid`.
367#[cfg(feature = "async")]
368pub struct AsyncOwnedSecurityResourceScope<'a> {
369    client: &'a AsyncMoexClient,
370    security: SecId,
371}
372
373#[derive(Clone)]
374/// Асинхронный scope по `engine/market/board/security`, владеющий значениями.
375#[cfg(feature = "async")]
376pub struct AsyncOwnedSecurityScope<'a> {
377    client: &'a AsyncMoexClient,
378    engine: EngineName,
379    market: MarketName,
380    board: BoardId,
381    security: SecId,
382}
383
384#[derive(Clone)]
385/// Blocking scope по `indexid`, владеющий значением.
386///
387/// Полезен для ergonomic-chain, где вход передаётся как `impl TryInto<IndexId>`.
388#[cfg(feature = "blocking")]
389pub struct OwnedIndexScope<'a> {
390    client: &'a BlockingMoexClient,
391    indexid: IndexId,
392}
393
394#[derive(Clone)]
395/// Blocking scope по `engine`, владеющий значением.
396#[cfg(feature = "blocking")]
397pub struct OwnedEngineScope<'a> {
398    client: &'a BlockingMoexClient,
399    engine: EngineName,
400}
401
402#[derive(Clone)]
403/// Blocking scope по `engine/market`, владеющий значениями.
404#[cfg(feature = "blocking")]
405pub struct OwnedMarketScope<'a> {
406    client: &'a BlockingMoexClient,
407    engine: EngineName,
408    market: MarketName,
409}
410
411#[derive(Clone)]
412/// Blocking scope по `engine/market/security`, владеющий значениями.
413#[cfg(feature = "blocking")]
414pub struct OwnedMarketSecurityScope<'a> {
415    client: &'a BlockingMoexClient,
416    engine: EngineName,
417    market: MarketName,
418    security: SecId,
419}
420
421#[derive(Clone)]
422/// Blocking scope по `engine/market/board`, владеющий значениями.
423#[cfg(feature = "blocking")]
424pub struct OwnedBoardScope<'a> {
425    client: &'a BlockingMoexClient,
426    engine: EngineName,
427    market: MarketName,
428    board: BoardId,
429}
430
431#[derive(Clone)]
432/// Blocking scope по `securities/{secid}`, владеющий `secid`.
433#[cfg(feature = "blocking")]
434pub struct OwnedSecurityResourceScope<'a> {
435    client: &'a BlockingMoexClient,
436    security: SecId,
437}
438
439#[derive(Clone)]
440/// Blocking scope по `engine/market/board/security`, владеющий значениями.
441#[cfg(feature = "blocking")]
442pub struct OwnedSecurityScope<'a> {
443    client: &'a BlockingMoexClient,
444    engine: EngineName,
445    market: MarketName,
446    board: BoardId,
447    security: SecId,
448}
449
450struct PaginationTracker<K> {
451    endpoint: Box<str>,
452    page_limit: NonZeroU32,
453    repeat_page_policy: RepeatPagePolicy,
454    start: u32,
455    first_key_on_previous_page: Option<K>,
456    finished: bool,
457}
458
459enum PaginationAdvance {
460    YieldPage,
461    EndOfPages,
462}
463
464#[cfg(any(feature = "blocking", feature = "async"))]
465fn resolve_base_url_or_default(base_url: Option<Url>) -> Result<Url, MoexError> {
466    match base_url {
467        Some(base_url) => Ok(base_url),
468        None => Url::parse(BASE_URL).map_err(|source| MoexError::InvalidBaseUrl {
469            base_url: BASE_URL,
470            reason: source.to_string(),
471        }),
472    }
473}
474
475#[cfg(feature = "blocking")]
476fn resolve_blocking_http_client(
477    client: Option<Client>,
478    http_client: ClientBuilder,
479) -> Result<Client, MoexError> {
480    match client {
481        Some(client) => Ok(client),
482        None => http_client
483            .build()
484            .map_err(|source| MoexError::BuildHttpClient { source }),
485    }
486}
487
488#[cfg(feature = "async")]
489fn resolve_async_http_client(
490    client: Option<reqwest::Client>,
491    http_client: reqwest::ClientBuilder,
492) -> Result<reqwest::Client, MoexError> {
493    match client {
494        Some(client) => Ok(client),
495        None => http_client
496            .build()
497            .map_err(|source| MoexError::BuildHttpClient { source }),
498    }
499}
500
501#[cfg(feature = "async")]
502fn resolve_async_rate_limit_state(
503    rate_limit: Option<RateLimit>,
504    rate_limit_sleep: Option<AsyncRateLimitSleep>,
505) -> Result<Option<AsyncRateLimitState>, MoexError> {
506    match rate_limit {
507        Some(limit) => {
508            let sleep = rate_limit_sleep.ok_or(MoexError::MissingAsyncRateLimitSleep)?;
509            Ok(Some(AsyncRateLimitState {
510                limiter: Mutex::new(RateLimiter::new(limit)),
511                sleep,
512            }))
513        }
514        None => Ok(None),
515    }
516}
517
518#[cfg(feature = "blocking")]
519impl BlockingMoexClientBuilder {
520    /// Включить или отключить выдачу `iss.meta`.
521    pub fn metadata(mut self, metadata: bool) -> Self {
522        self.metadata = metadata;
523        self
524    }
525
526    /// Задать явный базовый URL ISS.
527    pub fn base_url(mut self, base_url: Url) -> Self {
528        self.base_url = Some(base_url);
529        self
530    }
531
532    /// Передать готовый `reqwest::blocking::Client`.
533    pub fn client(mut self, client: Client) -> Self {
534        self.client = Some(client);
535        self
536    }
537
538    /// Установить общий таймаут HTTP-запросов.
539    pub fn timeout(mut self, timeout: Duration) -> Self {
540        self.http_client = self.http_client.timeout(timeout);
541        self
542    }
543
544    /// Установить таймаут установления TCP-соединения.
545    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
546        self.http_client = self.http_client.connect_timeout(timeout);
547        self
548    }
549
550    /// Установить заголовок `User-Agent` для всех запросов.
551    pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
552        self.http_client = self.http_client.user_agent(user_agent.into());
553        self
554    }
555
556    /// Установить `User-Agent` в формате `{crate_name}/{crate_version}`.
557    pub fn user_agent_from_crate(self) -> Self {
558        self.user_agent(format!(
559            "{}/{}",
560            env!("CARGO_PKG_NAME"),
561            env!("CARGO_PKG_VERSION")
562        ))
563    }
564
565    /// Установить набор заголовков по умолчанию для всех запросов.
566    pub fn default_headers(mut self, headers: HeaderMap) -> Self {
567        self.http_client = self.http_client.default_headers(headers);
568        self
569    }
570
571    /// Добавить proxy для HTTP-клиента.
572    ///
573    /// Метод можно вызывать несколько раз, если требуется набор правил proxy-маршрутизации.
574    pub fn proxy(mut self, proxy: reqwest::Proxy) -> Self {
575        self.http_client = self.http_client.proxy(proxy);
576        self
577    }
578
579    /// Отключить использование proxy из окружения и системных настроек.
580    pub fn no_proxy(mut self) -> Self {
581        self.http_client = self.http_client.no_proxy();
582        self
583    }
584
585    /// Включить ограничение частоты запросов на уровне клиента.
586    ///
587    /// Лимит применяется ко всем endpoint-методам и raw-запросам этого экземпляра клиента.
588    pub fn rate_limit(mut self, rate_limit: RateLimit) -> Self {
589        self.rate_limit = Some(rate_limit);
590        self
591    }
592
593    /// Построить блокирующий клиент ISS.
594    pub fn build(self) -> Result<BlockingMoexClient, MoexError> {
595        let Self {
596            base_url,
597            metadata,
598            client,
599            http_client,
600            rate_limit,
601        } = self;
602        let base_url = resolve_base_url_or_default(base_url)?;
603        let client = resolve_blocking_http_client(client, http_client)?;
604        Ok(BlockingMoexClient::with_base_url_and_rate_limit(
605            client, base_url, metadata, rate_limit,
606        ))
607    }
608}
609
610#[cfg(feature = "async")]
611impl AsyncMoexClientBuilder {
612    /// Включить или отключить выдачу `iss.meta`.
613    pub fn metadata(mut self, metadata: bool) -> Self {
614        self.metadata = metadata;
615        self
616    }
617
618    /// Задать явный базовый URL ISS.
619    pub fn base_url(mut self, base_url: Url) -> Self {
620        self.base_url = Some(base_url);
621        self
622    }
623
624    /// Передать готовый `reqwest::Client`.
625    pub fn client(mut self, client: reqwest::Client) -> Self {
626        self.client = Some(client);
627        self
628    }
629
630    /// Установить общий таймаут HTTP-запросов.
631    pub fn timeout(mut self, timeout: Duration) -> Self {
632        self.http_client = self.http_client.timeout(timeout);
633        self
634    }
635
636    /// Установить таймаут установления TCP-соединения.
637    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
638        self.http_client = self.http_client.connect_timeout(timeout);
639        self
640    }
641
642    /// Установить заголовок `User-Agent` для всех запросов.
643    pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
644        self.http_client = self.http_client.user_agent(user_agent.into());
645        self
646    }
647
648    /// Установить `User-Agent` в формате `{crate_name}/{crate_version}`.
649    pub fn user_agent_from_crate(self) -> Self {
650        self.user_agent(format!(
651            "{}/{}",
652            env!("CARGO_PKG_NAME"),
653            env!("CARGO_PKG_VERSION")
654        ))
655    }
656
657    /// Установить набор заголовков по умолчанию для всех запросов.
658    pub fn default_headers(mut self, headers: HeaderMap) -> Self {
659        self.http_client = self.http_client.default_headers(headers);
660        self
661    }
662
663    /// Добавить proxy для HTTP-клиента.
664    ///
665    /// Метод можно вызывать несколько раз, если требуется набор правил proxy-маршрутизации.
666    pub fn proxy(mut self, proxy: reqwest::Proxy) -> Self {
667        self.http_client = self.http_client.proxy(proxy);
668        self
669    }
670
671    /// Отключить использование proxy из окружения и системных настроек.
672    pub fn no_proxy(mut self) -> Self {
673        self.http_client = self.http_client.no_proxy();
674        self
675    }
676
677    /// Включить ограничение частоты запросов на уровне клиента.
678    ///
679    /// Для применения задержек нужно дополнительно передать `sleep` через
680    /// [`Self::rate_limit_sleep`].
681    pub fn rate_limit(mut self, rate_limit: RateLimit) -> Self {
682        self.rate_limit = Some(rate_limit);
683        self
684    }
685
686    /// Задать async-функцию ожидания для использования с [`Self::rate_limit`].
687    ///
688    /// Обычно это функция runtime-а, например `tokio::time::sleep`.
689    pub fn rate_limit_sleep<F, Fut>(mut self, sleep: F) -> Self
690    where
691        F: Fn(Duration) -> Fut + Send + Sync + 'static,
692        Fut: std::future::Future<Output = ()> + 'static,
693    {
694        self.rate_limit_sleep = Some(std::sync::Arc::new(move |delay| Box::pin(sleep(delay))));
695        self
696    }
697
698    /// Построить асинхронный клиент ISS.
699    pub fn build(self) -> Result<AsyncMoexClient, MoexError> {
700        let Self {
701            base_url,
702            metadata,
703            client,
704            http_client,
705            rate_limit,
706            rate_limit_sleep,
707        } = self;
708        let base_url = resolve_base_url_or_default(base_url)?;
709        let client = resolve_async_http_client(client, http_client)?;
710        let rate_limit = resolve_async_rate_limit_state(rate_limit, rate_limit_sleep)?;
711        Ok(AsyncMoexClient::with_base_url_and_rate_limit(
712            client, base_url, metadata, rate_limit,
713        ))
714    }
715}
716
717#[cfg(feature = "blocking")]
718impl BlockingMoexClient {
719    /// Создать builder для конфигурации клиента ISS.
720    pub fn builder() -> BlockingMoexClientBuilder {
721        BlockingMoexClientBuilder {
722            base_url: None,
723            metadata: false,
724            client: None,
725            http_client: Client::builder(),
726            rate_limit: None,
727        }
728    }
729
730    /// Создать клиент с базовым URL ISS по умолчанию (`iss.meta=off`).
731    pub fn new() -> Result<Self, MoexError> {
732        Self::builder().build()
733    }
734
735    /// Создать клиент с базовым URL ISS по умолчанию и `iss.meta=on`.
736    pub fn new_with_metadata() -> Result<Self, MoexError> {
737        Self::builder().metadata(true).build()
738    }
739
740    /// Создать клиент на базе переданного `reqwest`-клиента (`iss.meta=off`).
741    ///
742    /// Позволяет переиспользовать настройки таймаутов, прокси и TLS.
743    pub fn with_client(client: Client) -> Result<Self, MoexError> {
744        Self::builder().client(client).build()
745    }
746
747    /// Создать клиент на базе переданного `reqwest`-клиента и включить `iss.meta`.
748    pub fn with_client_with_metadata(client: Client) -> Result<Self, MoexError> {
749        Self::builder().metadata(true).client(client).build()
750    }
751
752    /// Создать клиент с явным базовым URL и готовым HTTP-клиентом (`iss.meta=off`).
753    pub fn with_base_url(client: Client, base_url: Url) -> Self {
754        Self::with_base_url_and_rate_limit(client, base_url, false, None)
755    }
756
757    /// Создать клиент с явным базовым URL, готовым HTTP-клиентом и `iss.meta=on`.
758    pub fn with_base_url_with_metadata(client: Client, base_url: Url) -> Self {
759        Self::with_base_url_and_rate_limit(client, base_url, true, None)
760    }
761
762    /// Текущее ограничение частоты запросов, если оно включено.
763    pub fn rate_limit(&self) -> Option<RateLimit> {
764        self.rate_limiter
765            .as_ref()
766            .map(|limiter| lock_rate_limiter(limiter).limit())
767    }
768
769    fn with_base_url_and_rate_limit(
770        client: Client,
771        base_url: Url,
772        metadata: bool,
773        rate_limit: Option<RateLimit>,
774    ) -> Self {
775        Self {
776            base_url,
777            metadata,
778            client,
779            rate_limiter: rate_limit.map(|limit| Mutex::new(RateLimiter::new(limit))),
780        }
781    }
782
783    /// Создать raw-builder для произвольного ISS endpoint.
784    pub fn raw(&self) -> RawIssRequestBuilder<'_> {
785        RawIssRequestBuilder {
786            client: self,
787            path: None,
788            query: Vec::new(),
789        }
790    }
791
792    /// Создать raw-builder для типизированного ISS endpoint-а.
793    ///
794    /// Builder автоматически получает `path` и значение `iss.only` по умолчанию.
795    pub fn raw_endpoint(&self, endpoint: IssEndpoint<'_>) -> RawIssRequestBuilder<'_> {
796        let path = endpoint.path();
797        let request = self.raw().path(path);
798        match endpoint.default_table() {
799            Some(table) => request.only(table),
800            None => request,
801        }
802    }
803
804    /// Получить список индексов из таблицы `indices`.
805    pub fn indexes(&self) -> Result<Vec<Index>, MoexError> {
806        let payload = self.get_payload(
807            INDEXES_ENDPOINT,
808            &[
809                (ISS_META_PARAM, metadata_value(self.metadata)),
810                (ISS_ONLY_PARAM, "indices"),
811                (INDICES_COLUMNS_PARAM, INDICES_COLUMNS),
812            ],
813        )?;
814        decode_indexes_json_payload(&payload)
815    }
816
817    /// Получить состав индекса (`analytics`) с единым режимом выборки страниц.
818    pub fn index_analytics_query(
819        &self,
820        indexid: &IndexId,
821        page_request: PageRequest,
822    ) -> Result<Vec<IndexAnalytics>, MoexError> {
823        match page_request {
824            PageRequest::FirstPage => {
825                self.fetch_index_analytics_page(indexid, Pagination::default())
826            }
827            PageRequest::Page(pagination) => self.fetch_index_analytics_page(indexid, pagination),
828            PageRequest::All { page_limit } => {
829                self.index_analytics_pages(indexid, page_limit).all()
830            }
831        }
832    }
833
834    /// Создать ленивый paginator страниц `index_analytics`.
835    pub fn index_analytics_pages<'a>(
836        &'a self,
837        indexid: &'a IndexId,
838        page_limit: NonZeroU32,
839    ) -> IndexAnalyticsPages<'a> {
840        IndexAnalyticsPages {
841            client: self,
842            indexid,
843            pagination: PaginationTracker::new(
844                index_analytics_endpoint(indexid),
845                page_limit,
846                RepeatPagePolicy::Error,
847            ),
848        }
849    }
850
851    /// Получить обороты ISS (`/iss/turnovers`).
852    pub fn turnovers(&self) -> Result<Vec<Turnover>, MoexError> {
853        let payload = self.get_payload(
854            TURNOVERS_ENDPOINT,
855            &[
856                (ISS_META_PARAM, metadata_value(self.metadata)),
857                (ISS_ONLY_PARAM, "turnovers"),
858                (TURNOVERS_COLUMNS_PARAM, TURNOVERS_COLUMNS),
859            ],
860        )?;
861        decode_turnovers_json_with_endpoint(&payload, TURNOVERS_ENDPOINT)
862    }
863
864    /// Получить обороты ISS по движку (`/iss/engines/{engine}/turnovers`).
865    pub fn engine_turnovers(&self, engine: &EngineName) -> Result<Vec<Turnover>, MoexError> {
866        let endpoint = engine_turnovers_endpoint(engine);
867        let payload = self.get_payload(
868            endpoint.as_str(),
869            &[
870                (ISS_META_PARAM, metadata_value(self.metadata)),
871                (ISS_ONLY_PARAM, "turnovers"),
872                (TURNOVERS_COLUMNS_PARAM, TURNOVERS_COLUMNS),
873            ],
874        )?;
875        decode_turnovers_json_with_endpoint(&payload, endpoint.as_str())
876    }
877
878    #[cfg(feature = "news")]
879    /// Получить новости ISS (`sitenews`) с единым режимом выборки страниц.
880    pub fn sitenews_query(&self, page_request: PageRequest) -> Result<Vec<SiteNews>, MoexError> {
881        match page_request {
882            PageRequest::FirstPage => self.fetch_sitenews_page(Pagination::default()),
883            PageRequest::Page(pagination) => self.fetch_sitenews_page(pagination),
884            PageRequest::All { page_limit } => self.sitenews_pages(page_limit).all(),
885        }
886    }
887
888    #[cfg(feature = "news")]
889    /// Создать ленивый paginator страниц `sitenews`.
890    pub fn sitenews_pages<'a>(&'a self, page_limit: NonZeroU32) -> SiteNewsPages<'a> {
891        SiteNewsPages {
892            client: self,
893            pagination: PaginationTracker::new(
894                SITENEWS_ENDPOINT,
895                page_limit,
896                RepeatPagePolicy::Error,
897            ),
898        }
899    }
900
901    #[cfg(feature = "news")]
902    /// Получить события ISS (`events`) с единым режимом выборки страниц.
903    pub fn events_query(&self, page_request: PageRequest) -> Result<Vec<Event>, MoexError> {
904        match page_request {
905            PageRequest::FirstPage => self.fetch_events_page(Pagination::default()),
906            PageRequest::Page(pagination) => self.fetch_events_page(pagination),
907            PageRequest::All { page_limit } => self.events_pages(page_limit).all(),
908        }
909    }
910
911    #[cfg(feature = "news")]
912    /// Создать ленивый paginator страниц `events`.
913    pub fn events_pages<'a>(&'a self, page_limit: NonZeroU32) -> EventsPages<'a> {
914        EventsPages {
915            client: self,
916            pagination: PaginationTracker::new(
917                EVENTS_ENDPOINT,
918                page_limit,
919                RepeatPagePolicy::Error,
920            ),
921        }
922    }
923
924    /// Получить `secstats` с единым режимом выборки страниц.
925    pub fn secstats_query(
926        &self,
927        engine: &EngineName,
928        market: &MarketName,
929        page_request: PageRequest,
930    ) -> Result<Vec<SecStat>, MoexError> {
931        match page_request {
932            PageRequest::FirstPage => {
933                self.fetch_secstats_page(engine, market, Pagination::default())
934            }
935            PageRequest::Page(pagination) => self.fetch_secstats_page(engine, market, pagination),
936            PageRequest::All { page_limit } => {
937                self.secstats_pages(engine, market, page_limit).all()
938            }
939        }
940    }
941
942    /// Создать ленивый paginator страниц `secstats`.
943    pub fn secstats_pages<'a>(
944        &'a self,
945        engine: &'a EngineName,
946        market: &'a MarketName,
947        page_limit: NonZeroU32,
948    ) -> SecStatsPages<'a> {
949        SecStatsPages {
950            client: self,
951            engine,
952            market,
953            pagination: PaginationTracker::new(
954                secstats_endpoint(engine, market),
955                page_limit,
956                RepeatPagePolicy::Error,
957            ),
958        }
959    }
960
961    /// Получить доступные торговые движки ISS (`engines`).
962    pub fn engines(&self) -> Result<Vec<Engine>, MoexError> {
963        let payload = self.get_payload(
964            ENGINES_ENDPOINT,
965            &[
966                (ISS_META_PARAM, metadata_value(self.metadata)),
967                (ISS_ONLY_PARAM, "engines"),
968                (ENGINES_COLUMNS_PARAM, ENGINES_COLUMNS),
969            ],
970        )?;
971        decode_engines_json_payload(&payload)
972    }
973
974    /// Получить рынки (`markets`) для заданного движка.
975    pub fn markets(&self, engine: &EngineName) -> Result<Vec<Market>, MoexError> {
976        let endpoint = markets_endpoint(engine);
977        let payload = self.get_payload(
978            endpoint.as_str(),
979            &[
980                (ISS_META_PARAM, metadata_value(self.metadata)),
981                (ISS_ONLY_PARAM, "markets"),
982                (MARKETS_COLUMNS_PARAM, MARKETS_COLUMNS),
983            ],
984        )?;
985        decode_markets_json_with_endpoint(&payload, endpoint.as_str())
986    }
987
988    /// Получить режимы торгов (`boards`) для пары движок/рынок.
989    pub fn boards(
990        &self,
991        engine: &EngineName,
992        market: &MarketName,
993    ) -> Result<Vec<Board>, MoexError> {
994        let endpoint = boards_endpoint(engine, market);
995        let payload = self.get_payload(
996            endpoint.as_str(),
997            &[
998                (ISS_META_PARAM, metadata_value(self.metadata)),
999                (ISS_ONLY_PARAM, "boards"),
1000                (BOARDS_COLUMNS_PARAM, BOARDS_COLUMNS),
1001            ],
1002        )?;
1003        decode_boards_json_with_endpoint(&payload, endpoint.as_str())
1004    }
1005
1006    /// Получить режимы торгов инструмента (`boards`) из endpoint `securities/{secid}`.
1007    pub fn security_boards(&self, security: &SecId) -> Result<Vec<SecurityBoard>, MoexError> {
1008        let endpoint = security_boards_endpoint(security);
1009        let payload = self.get_payload(
1010            endpoint.as_str(),
1011            &[
1012                (ISS_META_PARAM, metadata_value(self.metadata)),
1013                (ISS_ONLY_PARAM, "boards"),
1014                (BOARDS_COLUMNS_PARAM, SECURITY_BOARDS_COLUMNS),
1015            ],
1016        )?;
1017        decode_security_boards_json_with_endpoint(&payload, endpoint.as_str())
1018    }
1019
1020    /// Получить карточку инструмента (`securities`) из endpoint `securities/{secid}`.
1021    ///
1022    /// Возвращает `Ok(None)`, если таблица `securities` пустая.
1023    pub fn security_info(&self, security: &SecId) -> Result<Option<Security>, MoexError> {
1024        let endpoint = security_endpoint(security);
1025        let payload = self.get_payload(
1026            endpoint.as_str(),
1027            &[
1028                (ISS_META_PARAM, metadata_value(self.metadata)),
1029                (ISS_ONLY_PARAM, "securities"),
1030                (SECURITIES_COLUMNS_PARAM, SECURITIES_COLUMNS),
1031            ],
1032        )?;
1033        let securities = decode_securities_json_with_endpoint(&payload, endpoint.as_str())?;
1034        optional_single_security(endpoint.as_str(), securities)
1035    }
1036
1037    #[cfg(feature = "history")]
1038    /// Получить диапазон доступных исторических дат по инструменту и board.
1039    ///
1040    /// Возвращает `Ok(None)`, если таблица `dates` пустая.
1041    pub fn history_dates(
1042        &self,
1043        engine: &EngineName,
1044        market: &MarketName,
1045        board: &BoardId,
1046        security: &SecId,
1047    ) -> Result<Option<HistoryDates>, MoexError> {
1048        let endpoint = history_dates_endpoint(engine, market, board, security);
1049        let payload = self.get_payload(
1050            endpoint.as_str(),
1051            &[
1052                (ISS_META_PARAM, metadata_value(self.metadata)),
1053                (ISS_ONLY_PARAM, "dates"),
1054            ],
1055        )?;
1056        let dates = decode_history_dates_json_with_endpoint(&payload, endpoint.as_str())?;
1057        optional_single_history_dates(endpoint.as_str(), dates)
1058    }
1059
1060    #[cfg(feature = "history")]
1061    /// Получить исторические данные (`history`) с единым режимом выборки страниц.
1062    pub fn history_query(
1063        &self,
1064        engine: &EngineName,
1065        market: &MarketName,
1066        board: &BoardId,
1067        security: &SecId,
1068        page_request: PageRequest,
1069    ) -> Result<Vec<HistoryRecord>, MoexError> {
1070        match page_request {
1071            PageRequest::FirstPage => {
1072                self.fetch_history_page(engine, market, board, security, Pagination::default())
1073            }
1074            PageRequest::Page(pagination) => {
1075                self.fetch_history_page(engine, market, board, security, pagination)
1076            }
1077            PageRequest::All { page_limit } => self
1078                .history_pages(engine, market, board, security, page_limit)
1079                .all(),
1080        }
1081    }
1082
1083    #[cfg(feature = "history")]
1084    /// Создать ленивый paginator страниц `history`.
1085    pub fn history_pages<'a>(
1086        &'a self,
1087        engine: &'a EngineName,
1088        market: &'a MarketName,
1089        board: &'a BoardId,
1090        security: &'a SecId,
1091        page_limit: NonZeroU32,
1092    ) -> HistoryPages<'a> {
1093        HistoryPages {
1094            client: self,
1095            engine,
1096            market,
1097            board,
1098            security,
1099            pagination: PaginationTracker::new(
1100                history_endpoint(engine, market, board, security),
1101                page_limit,
1102                RepeatPagePolicy::Error,
1103            ),
1104        }
1105    }
1106
1107    /// Получить снимки инструментов (`LOTSIZE` и `LAST`) для режима торгов.
1108    pub fn board_snapshots(
1109        &self,
1110        engine: &EngineName,
1111        market: &MarketName,
1112        board: &BoardId,
1113    ) -> Result<Vec<SecuritySnapshot>, MoexError> {
1114        let endpoint = securities_endpoint(engine, market, board);
1115        let payload = self.get_payload(
1116            endpoint.as_str(),
1117            &[
1118                (ISS_META_PARAM, metadata_value(self.metadata)),
1119                (ISS_ONLY_PARAM, "securities,marketdata"),
1120                (SECURITIES_COLUMNS_PARAM, SECURITIES_SNAPSHOT_COLUMNS),
1121                (MARKETDATA_COLUMNS_PARAM, MARKETDATA_LAST_COLUMNS),
1122            ],
1123        )?;
1124        decode_board_security_snapshots_json_with_endpoint(&payload, endpoint.as_str())
1125    }
1126
1127    /// Получить снимки инструментов (`LOTSIZE` и `LAST`) по данным `SecurityBoard`.
1128    pub fn board_security_snapshots(
1129        &self,
1130        board: &SecurityBoard,
1131    ) -> Result<Vec<SecuritySnapshot>, MoexError> {
1132        self.board_snapshots(board.engine(), board.market(), board.boardid())
1133    }
1134
1135    /// Зафиксировать owning-scope по `engine` из ergonomic-входа.
1136    pub fn engine<E>(&self, engine: E) -> Result<OwnedEngineScope<'_>, ParseEngineNameError>
1137    where
1138        E: TryInto<EngineName>,
1139        E::Error: Into<ParseEngineNameError>,
1140    {
1141        let engine = engine.try_into().map_err(Into::into)?;
1142        Ok(OwnedEngineScope {
1143            client: self,
1144            engine,
1145        })
1146    }
1147
1148    /// Shortcut для часто используемого engine `stock`.
1149    pub fn stock(&self) -> Result<OwnedEngineScope<'_>, ParseEngineNameError> {
1150        self.engine("stock")
1151    }
1152
1153    /// Зафиксировать owning-scope по `indexid` из ergonomic-входа.
1154    pub fn index<I>(&self, indexid: I) -> Result<OwnedIndexScope<'_>, ParseIndexError>
1155    where
1156        I: TryInto<IndexId>,
1157        I::Error: Into<ParseIndexError>,
1158    {
1159        let indexid = indexid.try_into().map_err(Into::into)?;
1160        Ok(OwnedIndexScope {
1161            client: self,
1162            indexid,
1163        })
1164    }
1165
1166    /// Зафиксировать owning-scope по `secid` из ergonomic-входа.
1167    pub fn security<S>(
1168        &self,
1169        security: S,
1170    ) -> Result<OwnedSecurityResourceScope<'_>, ParseSecIdError>
1171    where
1172        S: TryInto<SecId>,
1173        S::Error: Into<ParseSecIdError>,
1174    {
1175        let security = security.try_into().map_err(Into::into)?;
1176        Ok(OwnedSecurityResourceScope {
1177            client: self,
1178            security,
1179        })
1180    }
1181
1182    /// Получить глобальный список инструментов (`/iss/securities`) с единым режимом выборки страниц.
1183    pub fn global_securities_query(
1184        &self,
1185        page_request: PageRequest,
1186    ) -> Result<Vec<Security>, MoexError> {
1187        match page_request {
1188            PageRequest::FirstPage => self.fetch_global_securities_page(Pagination::default()),
1189            PageRequest::Page(pagination) => self.fetch_global_securities_page(pagination),
1190            PageRequest::All { page_limit } => self.global_securities_pages(page_limit).all(),
1191        }
1192    }
1193
1194    /// Создать ленивый paginator страниц глобального `securities`.
1195    pub fn global_securities_pages<'a>(
1196        &'a self,
1197        page_limit: NonZeroU32,
1198    ) -> GlobalSecuritiesPages<'a> {
1199        GlobalSecuritiesPages {
1200            client: self,
1201            pagination: PaginationTracker::new(
1202                GLOBAL_SECURITIES_ENDPOINT,
1203                page_limit,
1204                RepeatPagePolicy::Error,
1205            ),
1206        }
1207    }
1208
1209    /// Получить карточку инструмента на уровне рынка (`.../markets/{market}/securities/{secid}`).
1210    ///
1211    /// Возвращает `Ok(None)`, если endpoint не содержит строк `securities`.
1212    pub fn market_security_info(
1213        &self,
1214        engine: &EngineName,
1215        market: &MarketName,
1216        security: &SecId,
1217    ) -> Result<Option<Security>, MoexError> {
1218        let endpoint = market_security_endpoint(engine, market, security);
1219        let payload = self.get_payload(
1220            endpoint.as_str(),
1221            &[
1222                (ISS_META_PARAM, metadata_value(self.metadata)),
1223                (ISS_ONLY_PARAM, "securities"),
1224                (SECURITIES_COLUMNS_PARAM, SECURITIES_COLUMNS),
1225            ],
1226        )?;
1227        let securities = decode_securities_json_with_endpoint(&payload, endpoint.as_str())?;
1228        optional_single_security(endpoint.as_str(), securities)
1229    }
1230
1231    /// Получить инструменты (`securities`) на уровне рынка с единым режимом выборки страниц.
1232    pub fn market_securities_query(
1233        &self,
1234        engine: &EngineName,
1235        market: &MarketName,
1236        page_request: PageRequest,
1237    ) -> Result<Vec<Security>, MoexError> {
1238        match page_request {
1239            PageRequest::FirstPage => {
1240                self.fetch_market_securities_page(engine, market, Pagination::default())
1241            }
1242            PageRequest::Page(pagination) => {
1243                self.fetch_market_securities_page(engine, market, pagination)
1244            }
1245            PageRequest::All { page_limit } => self
1246                .market_securities_pages(engine, market, page_limit)
1247                .all(),
1248        }
1249    }
1250
1251    /// Создать ленивый paginator страниц market-level `securities`.
1252    pub fn market_securities_pages<'a>(
1253        &'a self,
1254        engine: &'a EngineName,
1255        market: &'a MarketName,
1256        page_limit: NonZeroU32,
1257    ) -> MarketSecuritiesPages<'a> {
1258        MarketSecuritiesPages {
1259            client: self,
1260            engine,
1261            market,
1262            pagination: PaginationTracker::new(
1263                market_securities_endpoint(engine, market),
1264                page_limit,
1265                RepeatPagePolicy::Error,
1266            ),
1267        }
1268    }
1269
1270    /// Получить market-level стакан (`orderbook`) по первой странице ISS.
1271    pub fn market_orderbook(
1272        &self,
1273        engine: &EngineName,
1274        market: &MarketName,
1275    ) -> Result<Vec<OrderbookLevel>, MoexError> {
1276        let endpoint = market_orderbook_endpoint(engine, market);
1277        let payload = self.get_payload(
1278            endpoint.as_str(),
1279            &[
1280                (ISS_META_PARAM, metadata_value(self.metadata)),
1281                (ISS_ONLY_PARAM, "orderbook"),
1282                (ORDERBOOK_COLUMNS_PARAM, ORDERBOOK_COLUMNS),
1283            ],
1284        )?;
1285        decode_orderbook_json_with_endpoint(&payload, endpoint.as_str())
1286    }
1287
1288    /// Получить доступные границы свечей (`candleborders`) по инструменту.
1289    pub fn candle_borders(
1290        &self,
1291        engine: &EngineName,
1292        market: &MarketName,
1293        security: &SecId,
1294    ) -> Result<Vec<CandleBorder>, MoexError> {
1295        let endpoint = candleborders_endpoint(engine, market, security);
1296        let payload = self.get_payload(
1297            endpoint.as_str(),
1298            &[(ISS_META_PARAM, metadata_value(self.metadata))],
1299        )?;
1300        decode_candle_borders_json_with_endpoint(&payload, endpoint.as_str())
1301    }
1302
1303    /// Получить market-level сделки (`trades`) с единым режимом выборки страниц.
1304    pub fn market_trades_query(
1305        &self,
1306        engine: &EngineName,
1307        market: &MarketName,
1308        page_request: PageRequest,
1309    ) -> Result<Vec<Trade>, MoexError> {
1310        match page_request {
1311            PageRequest::FirstPage => {
1312                self.fetch_market_trades_page(engine, market, Pagination::default())
1313            }
1314            PageRequest::Page(pagination) => {
1315                self.fetch_market_trades_page(engine, market, pagination)
1316            }
1317            PageRequest::All { page_limit } => {
1318                self.market_trades_pages(engine, market, page_limit).all()
1319            }
1320        }
1321    }
1322
1323    /// Создать ленивый paginator страниц market-level `trades`.
1324    pub fn market_trades_pages<'a>(
1325        &'a self,
1326        engine: &'a EngineName,
1327        market: &'a MarketName,
1328        page_limit: NonZeroU32,
1329    ) -> MarketTradesPages<'a> {
1330        MarketTradesPages {
1331            client: self,
1332            engine,
1333            market,
1334            pagination: PaginationTracker::new(
1335                market_trades_endpoint(engine, market),
1336                page_limit,
1337                RepeatPagePolicy::Error,
1338            ),
1339        }
1340    }
1341
1342    /// Получить инструменты (`securities`) с единым режимом выборки страниц.
1343    ///
1344    /// `PageRequest::FirstPage` — только первая страница,
1345    /// `PageRequest::Page` — явные `start`/`limit`,
1346    /// `PageRequest::All` — полная выгрузка с авто-пагинацией.
1347    pub fn securities_query(
1348        &self,
1349        engine: &EngineName,
1350        market: &MarketName,
1351        board: &BoardId,
1352        page_request: PageRequest,
1353    ) -> Result<Vec<Security>, MoexError> {
1354        match page_request {
1355            PageRequest::FirstPage => {
1356                self.fetch_securities_page(engine, market, board, Pagination::default())
1357            }
1358            PageRequest::Page(pagination) => {
1359                self.fetch_securities_page(engine, market, board, pagination)
1360            }
1361            PageRequest::All { page_limit } => self
1362                .securities_pages(engine, market, board, page_limit)
1363                .all(),
1364        }
1365    }
1366
1367    /// Создать ленивый paginator страниц `securities`.
1368    pub fn securities_pages<'a>(
1369        &'a self,
1370        engine: &'a EngineName,
1371        market: &'a MarketName,
1372        board: &'a BoardId,
1373        page_limit: NonZeroU32,
1374    ) -> SecuritiesPages<'a> {
1375        SecuritiesPages {
1376            client: self,
1377            engine,
1378            market,
1379            board,
1380            pagination: PaginationTracker::new(
1381                securities_endpoint(engine, market, board),
1382                page_limit,
1383                RepeatPagePolicy::Error,
1384            ),
1385        }
1386    }
1387
1388    /// Получить текущий стакан (`orderbook`) по инструменту.
1389    pub fn orderbook(
1390        &self,
1391        engine: &EngineName,
1392        market: &MarketName,
1393        board: &BoardId,
1394        security: &SecId,
1395    ) -> Result<Vec<OrderbookLevel>, MoexError> {
1396        let endpoint = orderbook_endpoint(engine, market, board, security);
1397        let payload = self.get_payload(
1398            endpoint.as_str(),
1399            &[
1400                (ISS_META_PARAM, metadata_value(self.metadata)),
1401                (ISS_ONLY_PARAM, "orderbook"),
1402                (ORDERBOOK_COLUMNS_PARAM, ORDERBOOK_COLUMNS),
1403            ],
1404        )?;
1405        decode_orderbook_json_with_endpoint(&payload, endpoint.as_str())
1406    }
1407
1408    /// Получить свечи (`candles`) с единым режимом выборки страниц.
1409    pub fn candles_query(
1410        &self,
1411        engine: &EngineName,
1412        market: &MarketName,
1413        board: &BoardId,
1414        security: &SecId,
1415        query: CandleQuery,
1416        page_request: PageRequest,
1417    ) -> Result<Vec<Candle>, MoexError> {
1418        match page_request {
1419            PageRequest::FirstPage => self.fetch_candles_page(
1420                engine,
1421                market,
1422                board,
1423                security,
1424                query,
1425                Pagination::default(),
1426            ),
1427            PageRequest::Page(pagination) => {
1428                self.fetch_candles_page(engine, market, board, security, query, pagination)
1429            }
1430            PageRequest::All { page_limit } => self
1431                .candles_pages(engine, market, board, security, query, page_limit)
1432                .all(),
1433        }
1434    }
1435
1436    /// Создать ленивый paginator страниц `candles`.
1437    pub fn candles_pages<'a>(
1438        &'a self,
1439        engine: &'a EngineName,
1440        market: &'a MarketName,
1441        board: &'a BoardId,
1442        security: &'a SecId,
1443        query: CandleQuery,
1444        page_limit: NonZeroU32,
1445    ) -> CandlesPages<'a> {
1446        CandlesPages {
1447            client: self,
1448            engine,
1449            market,
1450            board,
1451            security,
1452            query,
1453            pagination: PaginationTracker::new(
1454                candles_endpoint(engine, market, board, security),
1455                page_limit,
1456                RepeatPagePolicy::Error,
1457            ),
1458        }
1459    }
1460
1461    /// Получить сделки (`trades`) с единым режимом выборки страниц.
1462    pub fn trades_query(
1463        &self,
1464        engine: &EngineName,
1465        market: &MarketName,
1466        board: &BoardId,
1467        security: &SecId,
1468        page_request: PageRequest,
1469    ) -> Result<Vec<Trade>, MoexError> {
1470        match page_request {
1471            PageRequest::FirstPage => {
1472                self.fetch_trades_page(engine, market, board, security, Pagination::default())
1473            }
1474            PageRequest::Page(pagination) => {
1475                self.fetch_trades_page(engine, market, board, security, pagination)
1476            }
1477            PageRequest::All { page_limit } => self
1478                .trades_pages(engine, market, board, security, page_limit)
1479                .all(),
1480        }
1481    }
1482
1483    /// Создать ленивый paginator страниц `trades`.
1484    pub fn trades_pages<'a>(
1485        &'a self,
1486        engine: &'a EngineName,
1487        market: &'a MarketName,
1488        board: &'a BoardId,
1489        security: &'a SecId,
1490        page_limit: NonZeroU32,
1491    ) -> TradesPages<'a> {
1492        TradesPages {
1493            client: self,
1494            engine,
1495            market,
1496            board,
1497            security,
1498            pagination: PaginationTracker::new(
1499                trades_endpoint(engine, market, board, security),
1500                page_limit,
1501                RepeatPagePolicy::Error,
1502            ),
1503        }
1504    }
1505
1506    fn fetch_securities_page(
1507        &self,
1508        engine: &EngineName,
1509        market: &MarketName,
1510        board: &BoardId,
1511        pagination: Pagination,
1512    ) -> Result<Vec<Security>, MoexError> {
1513        let endpoint = securities_endpoint(engine, market, board);
1514        let mut endpoint_url = self.endpoint_url(endpoint.as_str())?;
1515        {
1516            let mut query = endpoint_url.query_pairs_mut();
1517            query
1518                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
1519                .append_pair(ISS_ONLY_PARAM, "securities")
1520                .append_pair(SECURITIES_COLUMNS_PARAM, SECURITIES_COLUMNS);
1521        }
1522        append_pagination_to_url(&mut endpoint_url, pagination);
1523
1524        let payload = self.fetch_payload(endpoint.as_str(), endpoint_url)?;
1525        decode_securities_json_with_endpoint(&payload, endpoint.as_str())
1526    }
1527
1528    fn fetch_global_securities_page(
1529        &self,
1530        pagination: Pagination,
1531    ) -> Result<Vec<Security>, MoexError> {
1532        let endpoint = GLOBAL_SECURITIES_ENDPOINT;
1533        let mut endpoint_url = self.endpoint_url(endpoint)?;
1534        {
1535            let mut query = endpoint_url.query_pairs_mut();
1536            query
1537                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
1538                .append_pair(ISS_ONLY_PARAM, "securities")
1539                .append_pair(SECURITIES_COLUMNS_PARAM, SECURITIES_COLUMNS);
1540        }
1541        append_pagination_to_url(&mut endpoint_url, pagination);
1542
1543        let payload = self.fetch_payload(endpoint, endpoint_url)?;
1544        decode_securities_json_with_endpoint(&payload, endpoint)
1545    }
1546
1547    #[cfg(feature = "news")]
1548    fn fetch_sitenews_page(&self, pagination: Pagination) -> Result<Vec<SiteNews>, MoexError> {
1549        let endpoint = SITENEWS_ENDPOINT;
1550        let mut endpoint_url = self.endpoint_url(endpoint)?;
1551        {
1552            let mut query = endpoint_url.query_pairs_mut();
1553            query
1554                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
1555                .append_pair(ISS_ONLY_PARAM, "sitenews")
1556                .append_pair(SITENEWS_COLUMNS_PARAM, SITENEWS_COLUMNS);
1557        }
1558        append_pagination_to_url(&mut endpoint_url, pagination);
1559
1560        let payload = self.fetch_payload(endpoint, endpoint_url)?;
1561        decode_sitenews_json_with_endpoint(&payload, endpoint)
1562    }
1563
1564    #[cfg(feature = "news")]
1565    fn fetch_events_page(&self, pagination: Pagination) -> Result<Vec<Event>, MoexError> {
1566        let endpoint = EVENTS_ENDPOINT;
1567        let mut endpoint_url = self.endpoint_url(endpoint)?;
1568        {
1569            let mut query = endpoint_url.query_pairs_mut();
1570            query
1571                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
1572                .append_pair(ISS_ONLY_PARAM, "events")
1573                .append_pair(EVENTS_COLUMNS_PARAM, EVENTS_COLUMNS);
1574        }
1575        append_pagination_to_url(&mut endpoint_url, pagination);
1576
1577        let payload = self.fetch_payload(endpoint, endpoint_url)?;
1578        decode_events_json_with_endpoint(&payload, endpoint)
1579    }
1580
1581    fn fetch_market_securities_page(
1582        &self,
1583        engine: &EngineName,
1584        market: &MarketName,
1585        pagination: Pagination,
1586    ) -> Result<Vec<Security>, MoexError> {
1587        let endpoint = market_securities_endpoint(engine, market);
1588        let mut endpoint_url = self.endpoint_url(endpoint.as_str())?;
1589        {
1590            let mut query = endpoint_url.query_pairs_mut();
1591            query
1592                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
1593                .append_pair(ISS_ONLY_PARAM, "securities")
1594                .append_pair(SECURITIES_COLUMNS_PARAM, SECURITIES_COLUMNS);
1595        }
1596        append_pagination_to_url(&mut endpoint_url, pagination);
1597
1598        let payload = self.fetch_payload(endpoint.as_str(), endpoint_url)?;
1599        decode_securities_json_with_endpoint(&payload, endpoint.as_str())
1600    }
1601
1602    fn fetch_market_trades_page(
1603        &self,
1604        engine: &EngineName,
1605        market: &MarketName,
1606        pagination: Pagination,
1607    ) -> Result<Vec<Trade>, MoexError> {
1608        let endpoint = market_trades_endpoint(engine, market);
1609        let mut endpoint_url = self.endpoint_url(endpoint.as_str())?;
1610        {
1611            let mut query = endpoint_url.query_pairs_mut();
1612            query
1613                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
1614                .append_pair(ISS_ONLY_PARAM, "trades")
1615                .append_pair(TRADES_COLUMNS_PARAM, TRADES_COLUMNS);
1616        }
1617        append_pagination_to_url(&mut endpoint_url, pagination);
1618
1619        let payload = self.fetch_payload(endpoint.as_str(), endpoint_url)?;
1620        decode_trades_json_with_endpoint(&payload, endpoint.as_str())
1621    }
1622
1623    fn fetch_secstats_page(
1624        &self,
1625        engine: &EngineName,
1626        market: &MarketName,
1627        pagination: Pagination,
1628    ) -> Result<Vec<SecStat>, MoexError> {
1629        let endpoint = secstats_endpoint(engine, market);
1630        let mut endpoint_url = self.endpoint_url(endpoint.as_str())?;
1631        {
1632            let mut query = endpoint_url.query_pairs_mut();
1633            query
1634                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
1635                .append_pair(ISS_ONLY_PARAM, "secstats")
1636                .append_pair(SECSTATS_COLUMNS_PARAM, SECSTATS_COLUMNS);
1637        }
1638        append_pagination_to_url(&mut endpoint_url, pagination);
1639
1640        let payload = self.fetch_payload(endpoint.as_str(), endpoint_url)?;
1641        decode_secstats_json_with_endpoint(&payload, endpoint.as_str())
1642    }
1643
1644    fn fetch_index_analytics_page(
1645        &self,
1646        indexid: &IndexId,
1647        pagination: Pagination,
1648    ) -> Result<Vec<IndexAnalytics>, MoexError> {
1649        let endpoint = index_analytics_endpoint(indexid);
1650        let mut endpoint_url = self.endpoint_url(endpoint.as_str())?;
1651        {
1652            let mut query = endpoint_url.query_pairs_mut();
1653            query
1654                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
1655                .append_pair(ISS_ONLY_PARAM, "analytics")
1656                .append_pair(ANALYTICS_COLUMNS_PARAM, ANALYTICS_COLUMNS);
1657        }
1658        append_pagination_to_url(&mut endpoint_url, pagination);
1659
1660        let payload = self.fetch_payload(endpoint.as_str(), endpoint_url)?;
1661        decode_index_analytics_json_with_endpoint(&payload, endpoint.as_str())
1662    }
1663
1664    fn fetch_candles_page(
1665        &self,
1666        engine: &EngineName,
1667        market: &MarketName,
1668        board: &BoardId,
1669        security: &SecId,
1670        query: CandleQuery,
1671        pagination: Pagination,
1672    ) -> Result<Vec<Candle>, MoexError> {
1673        let endpoint = candles_endpoint(engine, market, board, security);
1674        let mut endpoint_url = self.endpoint_url(endpoint.as_str())?;
1675        {
1676            let mut query_pairs = endpoint_url.query_pairs_mut();
1677            query_pairs
1678                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
1679                .append_pair(ISS_ONLY_PARAM, "candles")
1680                .append_pair(CANDLES_COLUMNS_PARAM, CANDLES_COLUMNS);
1681        }
1682        append_candle_query_to_url(&mut endpoint_url, query);
1683        append_pagination_to_url(&mut endpoint_url, pagination);
1684
1685        let payload = self.fetch_payload(endpoint.as_str(), endpoint_url)?;
1686        decode_candles_json_with_endpoint(&payload, endpoint.as_str())
1687    }
1688
1689    fn fetch_trades_page(
1690        &self,
1691        engine: &EngineName,
1692        market: &MarketName,
1693        board: &BoardId,
1694        security: &SecId,
1695        pagination: Pagination,
1696    ) -> Result<Vec<Trade>, MoexError> {
1697        let endpoint = trades_endpoint(engine, market, board, security);
1698        let mut endpoint_url = self.endpoint_url(endpoint.as_str())?;
1699        {
1700            let mut query = endpoint_url.query_pairs_mut();
1701            query
1702                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
1703                .append_pair(ISS_ONLY_PARAM, "trades")
1704                .append_pair(TRADES_COLUMNS_PARAM, TRADES_COLUMNS);
1705        }
1706        append_pagination_to_url(&mut endpoint_url, pagination);
1707
1708        let payload = self.fetch_payload(endpoint.as_str(), endpoint_url)?;
1709        decode_trades_json_with_endpoint(&payload, endpoint.as_str())
1710    }
1711
1712    #[cfg(feature = "history")]
1713    fn fetch_history_page(
1714        &self,
1715        engine: &EngineName,
1716        market: &MarketName,
1717        board: &BoardId,
1718        security: &SecId,
1719        pagination: Pagination,
1720    ) -> Result<Vec<HistoryRecord>, MoexError> {
1721        let endpoint = history_endpoint(engine, market, board, security);
1722        let mut endpoint_url = self.endpoint_url(endpoint.as_str())?;
1723        {
1724            let mut query = endpoint_url.query_pairs_mut();
1725            query
1726                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
1727                .append_pair(ISS_ONLY_PARAM, "history")
1728                .append_pair(HISTORY_COLUMNS_PARAM, HISTORY_COLUMNS);
1729        }
1730        append_pagination_to_url(&mut endpoint_url, pagination);
1731
1732        let payload = self.fetch_payload(endpoint.as_str(), endpoint_url)?;
1733        decode_history_json_with_endpoint(&payload, endpoint.as_str())
1734    }
1735
1736    #[cfg(test)]
1737    pub(super) fn collect_paginated<T, K, F, G>(
1738        endpoint: &str,
1739        page_limit: NonZeroU32,
1740        repeat_page_policy: RepeatPagePolicy,
1741        mut fetch_page: F,
1742        first_key_of: G,
1743    ) -> Result<Vec<T>, MoexError>
1744    where
1745        F: FnMut(Pagination) -> Result<Vec<T>, MoexError>,
1746        G: Fn(&T) -> K,
1747        K: Eq,
1748    {
1749        let mut pagination = PaginationTracker::new(endpoint, page_limit, repeat_page_policy);
1750        let mut items = Vec::new();
1751
1752        while let Some(paging) = pagination.next_page_request() {
1753            let page = fetch_page(paging)?;
1754            let first_key_on_page = page.first().map(&first_key_of);
1755            match pagination.advance(page.len(), first_key_on_page)? {
1756                PaginationAdvance::YieldPage => items.extend(page),
1757                PaginationAdvance::EndOfPages => break,
1758            }
1759        }
1760
1761        Ok(items)
1762    }
1763
1764    fn endpoint_url(&self, endpoint: &str) -> Result<Url, MoexError> {
1765        self.base_url
1766            .join(endpoint)
1767            .map_err(|source| MoexError::EndpointUrl {
1768                endpoint: endpoint.to_owned().into_boxed_str(),
1769                reason: source.to_string(),
1770            })
1771    }
1772
1773    fn get_payload(
1774        &self,
1775        endpoint: &str,
1776        query_params: &[(&'static str, &'static str)],
1777    ) -> Result<String, MoexError> {
1778        let mut endpoint_url = self.endpoint_url(endpoint)?;
1779        {
1780            let mut url_query = endpoint_url.query_pairs_mut();
1781            for (key, value) in query_params {
1782                url_query.append_pair(key, value);
1783            }
1784        }
1785        self.fetch_payload(endpoint, endpoint_url)
1786    }
1787
1788    fn fetch_payload(&self, endpoint: &str, endpoint_url: Url) -> Result<String, MoexError> {
1789        self.wait_for_rate_limit();
1790        let response =
1791            self.client
1792                .get(endpoint_url)
1793                .send()
1794                .map_err(|source| MoexError::Request {
1795                    endpoint: endpoint.to_owned().into_boxed_str(),
1796                    source,
1797                })?;
1798        let status = response.status();
1799
1800        let content_type = response
1801            .headers()
1802            .get(reqwest::header::CONTENT_TYPE)
1803            .and_then(|value| value.to_str().ok())
1804            .map(|value| value.to_owned().into_boxed_str());
1805
1806        let payload = response.text().map_err(|source| MoexError::ReadBody {
1807            endpoint: endpoint.to_owned().into_boxed_str(),
1808            source,
1809        })?;
1810
1811        if !status.is_success() {
1812            return Err(MoexError::HttpStatus {
1813                endpoint: endpoint.to_owned().into_boxed_str(),
1814                status,
1815                content_type,
1816                body_prefix: truncate_prefix(&payload, NON_JSON_BODY_PREFIX_CHARS),
1817            });
1818        }
1819
1820        if !looks_like_json_payload(content_type.as_deref(), &payload) {
1821            return Err(MoexError::NonJsonPayload {
1822                endpoint: endpoint.to_owned().into_boxed_str(),
1823                content_type,
1824                body_prefix: truncate_prefix(&payload, NON_JSON_BODY_PREFIX_CHARS),
1825            });
1826        }
1827
1828        Ok(payload)
1829    }
1830
1831    fn wait_for_rate_limit(&self) {
1832        let Some(limiter) = &self.rate_limiter else {
1833            return;
1834        };
1835        let delay = reserve_rate_limit_delay(limiter);
1836        if !delay.is_zero() {
1837            std::thread::sleep(delay);
1838        }
1839    }
1840}
1841
1842#[cfg(any(feature = "blocking", feature = "async"))]
1843fn lock_rate_limiter(limiter: &Mutex<RateLimiter>) -> std::sync::MutexGuard<'_, RateLimiter> {
1844    match limiter.lock() {
1845        Ok(guard) => guard,
1846        Err(poisoned) => poisoned.into_inner(),
1847    }
1848}
1849
1850#[cfg(any(feature = "blocking", feature = "async"))]
1851fn reserve_rate_limit_delay(limiter: &Mutex<RateLimiter>) -> Duration {
1852    let mut limiter = lock_rate_limiter(limiter);
1853    limiter.reserve_delay()
1854}
1855
1856#[cfg(feature = "async")]
1857impl AsyncMoexClient {
1858    /// Создать builder для конфигурации асинхронного клиента ISS.
1859    pub fn builder() -> AsyncMoexClientBuilder {
1860        AsyncMoexClientBuilder {
1861            base_url: None,
1862            metadata: false,
1863            client: None,
1864            http_client: reqwest::Client::builder(),
1865            rate_limit: None,
1866            rate_limit_sleep: None,
1867        }
1868    }
1869
1870    /// Создать асинхронный клиент с базовым URL ISS по умолчанию (`iss.meta=off`).
1871    pub fn new() -> Result<Self, MoexError> {
1872        Self::builder().build()
1873    }
1874
1875    /// Создать асинхронный клиент с базовым URL ISS по умолчанию и `iss.meta=on`.
1876    pub fn new_with_metadata() -> Result<Self, MoexError> {
1877        Self::builder().metadata(true).build()
1878    }
1879
1880    /// Создать асинхронный клиент на базе переданного `reqwest`-клиента (`iss.meta=off`).
1881    pub fn with_client(client: reqwest::Client) -> Result<Self, MoexError> {
1882        Self::builder().client(client).build()
1883    }
1884
1885    /// Создать асинхронный клиент на базе переданного `reqwest`-клиента и включить `iss.meta`.
1886    pub fn with_client_with_metadata(client: reqwest::Client) -> Result<Self, MoexError> {
1887        Self::builder().metadata(true).client(client).build()
1888    }
1889
1890    /// Создать асинхронный клиент с явным базовым URL и готовым HTTP-клиентом (`iss.meta=off`).
1891    pub fn with_base_url(client: reqwest::Client, base_url: Url) -> Self {
1892        Self::with_base_url_and_rate_limit(client, base_url, false, None)
1893    }
1894
1895    /// Создать асинхронный клиент с явным базовым URL, HTTP-клиентом и `iss.meta=on`.
1896    pub fn with_base_url_with_metadata(client: reqwest::Client, base_url: Url) -> Self {
1897        Self::with_base_url_and_rate_limit(client, base_url, true, None)
1898    }
1899
1900    /// Текущее ограничение частоты запросов, если оно включено.
1901    pub fn rate_limit(&self) -> Option<RateLimit> {
1902        self.rate_limit
1903            .as_ref()
1904            .map(|rate_limit| lock_rate_limiter(&rate_limit.limiter).limit())
1905    }
1906
1907    fn with_base_url_and_rate_limit(
1908        client: reqwest::Client,
1909        base_url: Url,
1910        metadata: bool,
1911        rate_limit: Option<AsyncRateLimitState>,
1912    ) -> Self {
1913        Self {
1914            base_url,
1915            metadata,
1916            client,
1917            rate_limit,
1918        }
1919    }
1920
1921    /// Создать асинхронный raw-builder для произвольного ISS endpoint.
1922    pub fn raw(&self) -> AsyncRawIssRequestBuilder<'_> {
1923        AsyncRawIssRequestBuilder {
1924            client: self,
1925            path: None,
1926            query: Vec::new(),
1927        }
1928    }
1929
1930    /// Создать асинхронный raw-builder для типизированного ISS endpoint-а.
1931    ///
1932    /// Builder автоматически получает `path` и значение `iss.only` по умолчанию.
1933    pub fn raw_endpoint(&self, endpoint: IssEndpoint<'_>) -> AsyncRawIssRequestBuilder<'_> {
1934        let path = endpoint.path();
1935        let request = self.raw().path(path);
1936        match endpoint.default_table() {
1937            Some(table) => request.only(table),
1938            None => request,
1939        }
1940    }
1941
1942    /// Получить список индексов из таблицы `indices`.
1943    pub async fn indexes(&self) -> Result<Vec<Index>, MoexError> {
1944        let payload = self
1945            .get_payload(
1946                INDEXES_ENDPOINT,
1947                &[
1948                    (ISS_META_PARAM, metadata_value(self.metadata)),
1949                    (ISS_ONLY_PARAM, "indices"),
1950                    (INDICES_COLUMNS_PARAM, INDICES_COLUMNS),
1951                ],
1952            )
1953            .await?;
1954        decode_indexes_json_payload(&payload)
1955    }
1956
1957    /// Получить состав индекса (`analytics`) с единым режимом выборки страниц.
1958    pub async fn index_analytics_query(
1959        &self,
1960        indexid: &IndexId,
1961        page_request: PageRequest,
1962    ) -> Result<Vec<IndexAnalytics>, MoexError> {
1963        match page_request {
1964            PageRequest::FirstPage => {
1965                self.fetch_index_analytics_page(indexid, Pagination::default())
1966                    .await
1967            }
1968            PageRequest::Page(pagination) => {
1969                self.fetch_index_analytics_page(indexid, pagination).await
1970            }
1971            PageRequest::All { page_limit } => {
1972                self.index_analytics_pages(indexid, page_limit).all().await
1973            }
1974        }
1975    }
1976
1977    /// Создать асинхронный ленивый paginator страниц `index_analytics`.
1978    pub fn index_analytics_pages<'a>(
1979        &'a self,
1980        indexid: &'a IndexId,
1981        page_limit: NonZeroU32,
1982    ) -> AsyncIndexAnalyticsPages<'a> {
1983        AsyncIndexAnalyticsPages {
1984            client: self,
1985            indexid,
1986            pagination: PaginationTracker::new(
1987                index_analytics_endpoint(indexid),
1988                page_limit,
1989                RepeatPagePolicy::Error,
1990            ),
1991        }
1992    }
1993
1994    /// Получить обороты ISS (`/iss/turnovers`).
1995    pub async fn turnovers(&self) -> Result<Vec<Turnover>, MoexError> {
1996        let payload = self
1997            .get_payload(
1998                TURNOVERS_ENDPOINT,
1999                &[
2000                    (ISS_META_PARAM, metadata_value(self.metadata)),
2001                    (ISS_ONLY_PARAM, "turnovers"),
2002                    (TURNOVERS_COLUMNS_PARAM, TURNOVERS_COLUMNS),
2003                ],
2004            )
2005            .await?;
2006        decode_turnovers_json_with_endpoint(&payload, TURNOVERS_ENDPOINT)
2007    }
2008
2009    /// Получить обороты ISS по движку (`/iss/engines/{engine}/turnovers`).
2010    pub async fn engine_turnovers(&self, engine: &EngineName) -> Result<Vec<Turnover>, MoexError> {
2011        let endpoint = engine_turnovers_endpoint(engine);
2012        let payload = self
2013            .get_payload(
2014                endpoint.as_str(),
2015                &[
2016                    (ISS_META_PARAM, metadata_value(self.metadata)),
2017                    (ISS_ONLY_PARAM, "turnovers"),
2018                    (TURNOVERS_COLUMNS_PARAM, TURNOVERS_COLUMNS),
2019                ],
2020            )
2021            .await?;
2022        decode_turnovers_json_with_endpoint(&payload, endpoint.as_str())
2023    }
2024
2025    #[cfg(feature = "news")]
2026    /// Получить новости ISS (`sitenews`) с единым режимом выборки страниц.
2027    pub async fn sitenews_query(
2028        &self,
2029        page_request: PageRequest,
2030    ) -> Result<Vec<SiteNews>, MoexError> {
2031        match page_request {
2032            PageRequest::FirstPage => self.fetch_sitenews_page(Pagination::default()).await,
2033            PageRequest::Page(pagination) => self.fetch_sitenews_page(pagination).await,
2034            PageRequest::All { page_limit } => self.sitenews_pages(page_limit).all().await,
2035        }
2036    }
2037
2038    #[cfg(feature = "news")]
2039    /// Создать асинхронный ленивый paginator страниц `sitenews`.
2040    pub fn sitenews_pages<'a>(&'a self, page_limit: NonZeroU32) -> AsyncSiteNewsPages<'a> {
2041        AsyncSiteNewsPages {
2042            client: self,
2043            pagination: PaginationTracker::new(
2044                SITENEWS_ENDPOINT,
2045                page_limit,
2046                RepeatPagePolicy::Error,
2047            ),
2048        }
2049    }
2050
2051    #[cfg(feature = "news")]
2052    /// Получить события ISS (`events`) с единым режимом выборки страниц.
2053    pub async fn events_query(&self, page_request: PageRequest) -> Result<Vec<Event>, MoexError> {
2054        match page_request {
2055            PageRequest::FirstPage => self.fetch_events_page(Pagination::default()).await,
2056            PageRequest::Page(pagination) => self.fetch_events_page(pagination).await,
2057            PageRequest::All { page_limit } => self.events_pages(page_limit).all().await,
2058        }
2059    }
2060
2061    #[cfg(feature = "news")]
2062    /// Создать асинхронный ленивый paginator страниц `events`.
2063    pub fn events_pages<'a>(&'a self, page_limit: NonZeroU32) -> AsyncEventsPages<'a> {
2064        AsyncEventsPages {
2065            client: self,
2066            pagination: PaginationTracker::new(
2067                EVENTS_ENDPOINT,
2068                page_limit,
2069                RepeatPagePolicy::Error,
2070            ),
2071        }
2072    }
2073
2074    /// Получить `secstats` с единым режимом выборки страниц.
2075    pub async fn secstats_query(
2076        &self,
2077        engine: &EngineName,
2078        market: &MarketName,
2079        page_request: PageRequest,
2080    ) -> Result<Vec<SecStat>, MoexError> {
2081        match page_request {
2082            PageRequest::FirstPage => {
2083                self.fetch_secstats_page(engine, market, Pagination::default())
2084                    .await
2085            }
2086            PageRequest::Page(pagination) => {
2087                self.fetch_secstats_page(engine, market, pagination).await
2088            }
2089            PageRequest::All { page_limit } => {
2090                self.secstats_pages(engine, market, page_limit).all().await
2091            }
2092        }
2093    }
2094
2095    /// Создать асинхронный ленивый paginator страниц `secstats`.
2096    pub fn secstats_pages<'a>(
2097        &'a self,
2098        engine: &'a EngineName,
2099        market: &'a MarketName,
2100        page_limit: NonZeroU32,
2101    ) -> AsyncSecStatsPages<'a> {
2102        AsyncSecStatsPages {
2103            client: self,
2104            engine,
2105            market,
2106            pagination: PaginationTracker::new(
2107                secstats_endpoint(engine, market),
2108                page_limit,
2109                RepeatPagePolicy::Error,
2110            ),
2111        }
2112    }
2113
2114    /// Получить доступные торговые движки ISS (`engines`).
2115    pub async fn engines(&self) -> Result<Vec<Engine>, MoexError> {
2116        let payload = self
2117            .get_payload(
2118                ENGINES_ENDPOINT,
2119                &[
2120                    (ISS_META_PARAM, metadata_value(self.metadata)),
2121                    (ISS_ONLY_PARAM, "engines"),
2122                    (ENGINES_COLUMNS_PARAM, ENGINES_COLUMNS),
2123                ],
2124            )
2125            .await?;
2126        decode_engines_json_payload(&payload)
2127    }
2128
2129    /// Получить рынки (`markets`) для заданного движка.
2130    pub async fn markets(&self, engine: &EngineName) -> Result<Vec<Market>, MoexError> {
2131        let endpoint = markets_endpoint(engine);
2132        let payload = self
2133            .get_payload(
2134                endpoint.as_str(),
2135                &[
2136                    (ISS_META_PARAM, metadata_value(self.metadata)),
2137                    (ISS_ONLY_PARAM, "markets"),
2138                    (MARKETS_COLUMNS_PARAM, MARKETS_COLUMNS),
2139                ],
2140            )
2141            .await?;
2142        decode_markets_json_with_endpoint(&payload, endpoint.as_str())
2143    }
2144
2145    /// Получить режимы торгов (`boards`) для пары движок/рынок.
2146    pub async fn boards(
2147        &self,
2148        engine: &EngineName,
2149        market: &MarketName,
2150    ) -> Result<Vec<Board>, MoexError> {
2151        let endpoint = boards_endpoint(engine, market);
2152        let payload = self
2153            .get_payload(
2154                endpoint.as_str(),
2155                &[
2156                    (ISS_META_PARAM, metadata_value(self.metadata)),
2157                    (ISS_ONLY_PARAM, "boards"),
2158                    (BOARDS_COLUMNS_PARAM, BOARDS_COLUMNS),
2159                ],
2160            )
2161            .await?;
2162        decode_boards_json_with_endpoint(&payload, endpoint.as_str())
2163    }
2164
2165    /// Получить режимы торгов инструмента (`boards`) из endpoint `securities/{secid}`.
2166    pub async fn security_boards(&self, security: &SecId) -> Result<Vec<SecurityBoard>, MoexError> {
2167        let endpoint = security_boards_endpoint(security);
2168        let payload = self
2169            .get_payload(
2170                endpoint.as_str(),
2171                &[
2172                    (ISS_META_PARAM, metadata_value(self.metadata)),
2173                    (ISS_ONLY_PARAM, "boards"),
2174                    (BOARDS_COLUMNS_PARAM, SECURITY_BOARDS_COLUMNS),
2175                ],
2176            )
2177            .await?;
2178        decode_security_boards_json_with_endpoint(&payload, endpoint.as_str())
2179    }
2180
2181    /// Получить карточку инструмента (`securities`) из endpoint `securities/{secid}`.
2182    ///
2183    /// Возвращает `Ok(None)`, если таблица `securities` пустая.
2184    pub async fn security_info(&self, security: &SecId) -> Result<Option<Security>, MoexError> {
2185        let endpoint = security_endpoint(security);
2186        let payload = self
2187            .get_payload(
2188                endpoint.as_str(),
2189                &[
2190                    (ISS_META_PARAM, metadata_value(self.metadata)),
2191                    (ISS_ONLY_PARAM, "securities"),
2192                    (SECURITIES_COLUMNS_PARAM, SECURITIES_COLUMNS),
2193                ],
2194            )
2195            .await?;
2196        let securities = decode_securities_json_with_endpoint(&payload, endpoint.as_str())?;
2197        optional_single_security(endpoint.as_str(), securities)
2198    }
2199
2200    #[cfg(feature = "history")]
2201    /// Получить диапазон доступных исторических дат по инструменту и board.
2202    ///
2203    /// Возвращает `Ok(None)`, если таблица `dates` пустая.
2204    pub async fn history_dates(
2205        &self,
2206        engine: &EngineName,
2207        market: &MarketName,
2208        board: &BoardId,
2209        security: &SecId,
2210    ) -> Result<Option<HistoryDates>, MoexError> {
2211        let endpoint = history_dates_endpoint(engine, market, board, security);
2212        let payload = self
2213            .get_payload(
2214                endpoint.as_str(),
2215                &[
2216                    (ISS_META_PARAM, metadata_value(self.metadata)),
2217                    (ISS_ONLY_PARAM, "dates"),
2218                ],
2219            )
2220            .await?;
2221        let dates = decode_history_dates_json_with_endpoint(&payload, endpoint.as_str())?;
2222        optional_single_history_dates(endpoint.as_str(), dates)
2223    }
2224
2225    #[cfg(feature = "history")]
2226    /// Получить исторические данные (`history`) с единым режимом выборки страниц.
2227    pub async fn history_query(
2228        &self,
2229        engine: &EngineName,
2230        market: &MarketName,
2231        board: &BoardId,
2232        security: &SecId,
2233        page_request: PageRequest,
2234    ) -> Result<Vec<HistoryRecord>, MoexError> {
2235        match page_request {
2236            PageRequest::FirstPage => {
2237                self.fetch_history_page(engine, market, board, security, Pagination::default())
2238                    .await
2239            }
2240            PageRequest::Page(pagination) => {
2241                self.fetch_history_page(engine, market, board, security, pagination)
2242                    .await
2243            }
2244            PageRequest::All { page_limit } => {
2245                self.history_pages(engine, market, board, security, page_limit)
2246                    .all()
2247                    .await
2248            }
2249        }
2250    }
2251
2252    #[cfg(feature = "history")]
2253    /// Создать асинхронный ленивый paginator страниц `history`.
2254    pub fn history_pages<'a>(
2255        &'a self,
2256        engine: &'a EngineName,
2257        market: &'a MarketName,
2258        board: &'a BoardId,
2259        security: &'a SecId,
2260        page_limit: NonZeroU32,
2261    ) -> AsyncHistoryPages<'a> {
2262        AsyncHistoryPages {
2263            client: self,
2264            engine,
2265            market,
2266            board,
2267            security,
2268            pagination: PaginationTracker::new(
2269                history_endpoint(engine, market, board, security),
2270                page_limit,
2271                RepeatPagePolicy::Error,
2272            ),
2273        }
2274    }
2275
2276    /// Получить снимки инструментов (`LOTSIZE` и `LAST`) для режима торгов.
2277    pub async fn board_snapshots(
2278        &self,
2279        engine: &EngineName,
2280        market: &MarketName,
2281        board: &BoardId,
2282    ) -> Result<Vec<SecuritySnapshot>, MoexError> {
2283        let endpoint = securities_endpoint(engine, market, board);
2284        let payload = self
2285            .get_payload(
2286                endpoint.as_str(),
2287                &[
2288                    (ISS_META_PARAM, metadata_value(self.metadata)),
2289                    (ISS_ONLY_PARAM, "securities,marketdata"),
2290                    (SECURITIES_COLUMNS_PARAM, SECURITIES_SNAPSHOT_COLUMNS),
2291                    (MARKETDATA_COLUMNS_PARAM, MARKETDATA_LAST_COLUMNS),
2292                ],
2293            )
2294            .await?;
2295        decode_board_security_snapshots_json_with_endpoint(&payload, endpoint.as_str())
2296    }
2297
2298    /// Получить снимки инструментов (`LOTSIZE` и `LAST`) по данным `SecurityBoard`.
2299    pub async fn board_security_snapshots(
2300        &self,
2301        board: &SecurityBoard,
2302    ) -> Result<Vec<SecuritySnapshot>, MoexError> {
2303        self.board_snapshots(board.engine(), board.market(), board.boardid())
2304            .await
2305    }
2306
2307    /// Получить глобальный список инструментов (`/iss/securities`) с единым режимом выборки страниц.
2308    pub async fn global_securities_query(
2309        &self,
2310        page_request: PageRequest,
2311    ) -> Result<Vec<Security>, MoexError> {
2312        match page_request {
2313            PageRequest::FirstPage => {
2314                self.fetch_global_securities_page(Pagination::default())
2315                    .await
2316            }
2317            PageRequest::Page(pagination) => self.fetch_global_securities_page(pagination).await,
2318            PageRequest::All { page_limit } => self.global_securities_pages(page_limit).all().await,
2319        }
2320    }
2321
2322    /// Создать асинхронный ленивый paginator страниц глобального `securities`.
2323    pub fn global_securities_pages<'a>(
2324        &'a self,
2325        page_limit: NonZeroU32,
2326    ) -> AsyncGlobalSecuritiesPages<'a> {
2327        AsyncGlobalSecuritiesPages {
2328            client: self,
2329            pagination: PaginationTracker::new(
2330                GLOBAL_SECURITIES_ENDPOINT,
2331                page_limit,
2332                RepeatPagePolicy::Error,
2333            ),
2334        }
2335    }
2336
2337    /// Получить карточку инструмента на уровне рынка (`.../markets/{market}/securities/{secid}`).
2338    ///
2339    /// Возвращает `Ok(None)`, если endpoint не содержит строк `securities`.
2340    pub async fn market_security_info(
2341        &self,
2342        engine: &EngineName,
2343        market: &MarketName,
2344        security: &SecId,
2345    ) -> Result<Option<Security>, MoexError> {
2346        let endpoint = market_security_endpoint(engine, market, security);
2347        let payload = self
2348            .get_payload(
2349                endpoint.as_str(),
2350                &[
2351                    (ISS_META_PARAM, metadata_value(self.metadata)),
2352                    (ISS_ONLY_PARAM, "securities"),
2353                    (SECURITIES_COLUMNS_PARAM, SECURITIES_COLUMNS),
2354                ],
2355            )
2356            .await?;
2357        let securities = decode_securities_json_with_endpoint(&payload, endpoint.as_str())?;
2358        optional_single_security(endpoint.as_str(), securities)
2359    }
2360
2361    /// Получить инструменты (`securities`) на уровне рынка с единым режимом выборки страниц.
2362    pub async fn market_securities_query(
2363        &self,
2364        engine: &EngineName,
2365        market: &MarketName,
2366        page_request: PageRequest,
2367    ) -> Result<Vec<Security>, MoexError> {
2368        match page_request {
2369            PageRequest::FirstPage => {
2370                self.fetch_market_securities_page(engine, market, Pagination::default())
2371                    .await
2372            }
2373            PageRequest::Page(pagination) => {
2374                self.fetch_market_securities_page(engine, market, pagination)
2375                    .await
2376            }
2377            PageRequest::All { page_limit } => {
2378                self.market_securities_pages(engine, market, page_limit)
2379                    .all()
2380                    .await
2381            }
2382        }
2383    }
2384
2385    /// Создать асинхронный ленивый paginator страниц market-level `securities`.
2386    pub fn market_securities_pages<'a>(
2387        &'a self,
2388        engine: &'a EngineName,
2389        market: &'a MarketName,
2390        page_limit: NonZeroU32,
2391    ) -> AsyncMarketSecuritiesPages<'a> {
2392        AsyncMarketSecuritiesPages {
2393            client: self,
2394            engine,
2395            market,
2396            pagination: PaginationTracker::new(
2397                market_securities_endpoint(engine, market),
2398                page_limit,
2399                RepeatPagePolicy::Error,
2400            ),
2401        }
2402    }
2403
2404    /// Получить market-level стакан (`orderbook`) по первой странице ISS.
2405    pub async fn market_orderbook(
2406        &self,
2407        engine: &EngineName,
2408        market: &MarketName,
2409    ) -> Result<Vec<OrderbookLevel>, MoexError> {
2410        let endpoint = market_orderbook_endpoint(engine, market);
2411        let payload = self
2412            .get_payload(
2413                endpoint.as_str(),
2414                &[
2415                    (ISS_META_PARAM, metadata_value(self.metadata)),
2416                    (ISS_ONLY_PARAM, "orderbook"),
2417                    (ORDERBOOK_COLUMNS_PARAM, ORDERBOOK_COLUMNS),
2418                ],
2419            )
2420            .await?;
2421        decode_orderbook_json_with_endpoint(&payload, endpoint.as_str())
2422    }
2423
2424    /// Получить доступные границы свечей (`candleborders`) по инструменту.
2425    pub async fn candle_borders(
2426        &self,
2427        engine: &EngineName,
2428        market: &MarketName,
2429        security: &SecId,
2430    ) -> Result<Vec<CandleBorder>, MoexError> {
2431        let endpoint = candleborders_endpoint(engine, market, security);
2432        let payload = self
2433            .get_payload(
2434                endpoint.as_str(),
2435                &[(ISS_META_PARAM, metadata_value(self.metadata))],
2436            )
2437            .await?;
2438        decode_candle_borders_json_with_endpoint(&payload, endpoint.as_str())
2439    }
2440
2441    /// Получить market-level сделки (`trades`) с единым режимом выборки страниц.
2442    pub async fn market_trades_query(
2443        &self,
2444        engine: &EngineName,
2445        market: &MarketName,
2446        page_request: PageRequest,
2447    ) -> Result<Vec<Trade>, MoexError> {
2448        match page_request {
2449            PageRequest::FirstPage => {
2450                self.fetch_market_trades_page(engine, market, Pagination::default())
2451                    .await
2452            }
2453            PageRequest::Page(pagination) => {
2454                self.fetch_market_trades_page(engine, market, pagination)
2455                    .await
2456            }
2457            PageRequest::All { page_limit } => {
2458                self.market_trades_pages(engine, market, page_limit)
2459                    .all()
2460                    .await
2461            }
2462        }
2463    }
2464
2465    /// Создать асинхронный ленивый paginator страниц market-level `trades`.
2466    pub fn market_trades_pages<'a>(
2467        &'a self,
2468        engine: &'a EngineName,
2469        market: &'a MarketName,
2470        page_limit: NonZeroU32,
2471    ) -> AsyncMarketTradesPages<'a> {
2472        AsyncMarketTradesPages {
2473            client: self,
2474            engine,
2475            market,
2476            pagination: PaginationTracker::new(
2477                market_trades_endpoint(engine, market),
2478                page_limit,
2479                RepeatPagePolicy::Error,
2480            ),
2481        }
2482    }
2483
2484    /// Получить инструменты (`securities`) с единым режимом выборки страниц.
2485    pub async fn securities_query(
2486        &self,
2487        engine: &EngineName,
2488        market: &MarketName,
2489        board: &BoardId,
2490        page_request: PageRequest,
2491    ) -> Result<Vec<Security>, MoexError> {
2492        match page_request {
2493            PageRequest::FirstPage => {
2494                self.fetch_securities_page(engine, market, board, Pagination::default())
2495                    .await
2496            }
2497            PageRequest::Page(pagination) => {
2498                self.fetch_securities_page(engine, market, board, pagination)
2499                    .await
2500            }
2501            PageRequest::All { page_limit } => {
2502                self.securities_pages(engine, market, board, page_limit)
2503                    .all()
2504                    .await
2505            }
2506        }
2507    }
2508
2509    /// Создать асинхронный ленивый paginator страниц `securities`.
2510    pub fn securities_pages<'a>(
2511        &'a self,
2512        engine: &'a EngineName,
2513        market: &'a MarketName,
2514        board: &'a BoardId,
2515        page_limit: NonZeroU32,
2516    ) -> AsyncSecuritiesPages<'a> {
2517        AsyncSecuritiesPages {
2518            client: self,
2519            engine,
2520            market,
2521            board,
2522            pagination: PaginationTracker::new(
2523                securities_endpoint(engine, market, board),
2524                page_limit,
2525                RepeatPagePolicy::Error,
2526            ),
2527        }
2528    }
2529
2530    /// Получить текущий стакан (`orderbook`) по инструменту.
2531    pub async fn orderbook(
2532        &self,
2533        engine: &EngineName,
2534        market: &MarketName,
2535        board: &BoardId,
2536        security: &SecId,
2537    ) -> Result<Vec<OrderbookLevel>, MoexError> {
2538        let endpoint = orderbook_endpoint(engine, market, board, security);
2539        let payload = self
2540            .get_payload(
2541                endpoint.as_str(),
2542                &[
2543                    (ISS_META_PARAM, metadata_value(self.metadata)),
2544                    (ISS_ONLY_PARAM, "orderbook"),
2545                    (ORDERBOOK_COLUMNS_PARAM, ORDERBOOK_COLUMNS),
2546                ],
2547            )
2548            .await?;
2549        decode_orderbook_json_with_endpoint(&payload, endpoint.as_str())
2550    }
2551
2552    /// Получить свечи (`candles`) с единым режимом выборки страниц.
2553    pub async fn candles_query(
2554        &self,
2555        engine: &EngineName,
2556        market: &MarketName,
2557        board: &BoardId,
2558        security: &SecId,
2559        query: CandleQuery,
2560        page_request: PageRequest,
2561    ) -> Result<Vec<Candle>, MoexError> {
2562        match page_request {
2563            PageRequest::FirstPage => {
2564                self.fetch_candles_page(
2565                    engine,
2566                    market,
2567                    board,
2568                    security,
2569                    query,
2570                    Pagination::default(),
2571                )
2572                .await
2573            }
2574            PageRequest::Page(pagination) => {
2575                self.fetch_candles_page(engine, market, board, security, query, pagination)
2576                    .await
2577            }
2578            PageRequest::All { page_limit } => {
2579                self.candles_pages(engine, market, board, security, query, page_limit)
2580                    .all()
2581                    .await
2582            }
2583        }
2584    }
2585
2586    /// Создать асинхронный ленивый paginator страниц `candles`.
2587    pub fn candles_pages<'a>(
2588        &'a self,
2589        engine: &'a EngineName,
2590        market: &'a MarketName,
2591        board: &'a BoardId,
2592        security: &'a SecId,
2593        query: CandleQuery,
2594        page_limit: NonZeroU32,
2595    ) -> AsyncCandlesPages<'a> {
2596        AsyncCandlesPages {
2597            client: self,
2598            engine,
2599            market,
2600            board,
2601            security,
2602            query,
2603            pagination: PaginationTracker::new(
2604                candles_endpoint(engine, market, board, security),
2605                page_limit,
2606                RepeatPagePolicy::Error,
2607            ),
2608        }
2609    }
2610
2611    /// Получить сделки (`trades`) с единым режимом выборки страниц.
2612    pub async fn trades_query(
2613        &self,
2614        engine: &EngineName,
2615        market: &MarketName,
2616        board: &BoardId,
2617        security: &SecId,
2618        page_request: PageRequest,
2619    ) -> Result<Vec<Trade>, MoexError> {
2620        match page_request {
2621            PageRequest::FirstPage => {
2622                self.fetch_trades_page(engine, market, board, security, Pagination::default())
2623                    .await
2624            }
2625            PageRequest::Page(pagination) => {
2626                self.fetch_trades_page(engine, market, board, security, pagination)
2627                    .await
2628            }
2629            PageRequest::All { page_limit } => {
2630                self.trades_pages(engine, market, board, security, page_limit)
2631                    .all()
2632                    .await
2633            }
2634        }
2635    }
2636
2637    /// Создать асинхронный ленивый paginator страниц `trades`.
2638    pub fn trades_pages<'a>(
2639        &'a self,
2640        engine: &'a EngineName,
2641        market: &'a MarketName,
2642        board: &'a BoardId,
2643        security: &'a SecId,
2644        page_limit: NonZeroU32,
2645    ) -> AsyncTradesPages<'a> {
2646        AsyncTradesPages {
2647            client: self,
2648            engine,
2649            market,
2650            board,
2651            security,
2652            pagination: PaginationTracker::new(
2653                trades_endpoint(engine, market, board, security),
2654                page_limit,
2655                RepeatPagePolicy::Error,
2656            ),
2657        }
2658    }
2659
2660    /// Зафиксировать async owning-scope по `engine` из ergonomic-входа.
2661    pub fn engine<E>(&self, engine: E) -> Result<AsyncOwnedEngineScope<'_>, ParseEngineNameError>
2662    where
2663        E: TryInto<EngineName>,
2664        E::Error: Into<ParseEngineNameError>,
2665    {
2666        let engine = engine.try_into().map_err(Into::into)?;
2667        Ok(AsyncOwnedEngineScope {
2668            client: self,
2669            engine,
2670        })
2671    }
2672
2673    /// Shortcut для часто используемого engine `stock`.
2674    pub fn stock(&self) -> Result<AsyncOwnedEngineScope<'_>, ParseEngineNameError> {
2675        self.engine("stock")
2676    }
2677
2678    /// Зафиксировать async owning-scope по `indexid` из ergonomic-входа.
2679    pub fn index<I>(&self, indexid: I) -> Result<AsyncOwnedIndexScope<'_>, ParseIndexError>
2680    where
2681        I: TryInto<IndexId>,
2682        I::Error: Into<ParseIndexError>,
2683    {
2684        let indexid = indexid.try_into().map_err(Into::into)?;
2685        Ok(AsyncOwnedIndexScope {
2686            client: self,
2687            indexid,
2688        })
2689    }
2690
2691    /// Зафиксировать async owning-scope по `secid` из ergonomic-входа.
2692    pub fn security<S>(
2693        &self,
2694        security: S,
2695    ) -> Result<AsyncOwnedSecurityResourceScope<'_>, ParseSecIdError>
2696    where
2697        S: TryInto<SecId>,
2698        S::Error: Into<ParseSecIdError>,
2699    {
2700        let security = security.try_into().map_err(Into::into)?;
2701        Ok(AsyncOwnedSecurityResourceScope {
2702            client: self,
2703            security,
2704        })
2705    }
2706
2707    async fn fetch_index_analytics_page(
2708        &self,
2709        indexid: &IndexId,
2710        pagination: Pagination,
2711    ) -> Result<Vec<IndexAnalytics>, MoexError> {
2712        let endpoint = index_analytics_endpoint(indexid);
2713        let mut endpoint_url = self.endpoint_url(endpoint.as_str())?;
2714        {
2715            let mut query = endpoint_url.query_pairs_mut();
2716            query
2717                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
2718                .append_pair(ISS_ONLY_PARAM, "analytics")
2719                .append_pair(ANALYTICS_COLUMNS_PARAM, ANALYTICS_COLUMNS);
2720        }
2721        append_pagination_to_url(&mut endpoint_url, pagination);
2722
2723        let payload = self.fetch_payload(endpoint.as_str(), endpoint_url).await?;
2724        decode_index_analytics_json_with_endpoint(&payload, endpoint.as_str())
2725    }
2726
2727    async fn fetch_securities_page(
2728        &self,
2729        engine: &EngineName,
2730        market: &MarketName,
2731        board: &BoardId,
2732        pagination: Pagination,
2733    ) -> Result<Vec<Security>, MoexError> {
2734        let endpoint = securities_endpoint(engine, market, board);
2735        let mut endpoint_url = self.endpoint_url(endpoint.as_str())?;
2736        {
2737            let mut query = endpoint_url.query_pairs_mut();
2738            query
2739                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
2740                .append_pair(ISS_ONLY_PARAM, "securities")
2741                .append_pair(SECURITIES_COLUMNS_PARAM, SECURITIES_COLUMNS);
2742        }
2743        append_pagination_to_url(&mut endpoint_url, pagination);
2744
2745        let payload = self.fetch_payload(endpoint.as_str(), endpoint_url).await?;
2746        decode_securities_json_with_endpoint(&payload, endpoint.as_str())
2747    }
2748
2749    async fn fetch_global_securities_page(
2750        &self,
2751        pagination: Pagination,
2752    ) -> Result<Vec<Security>, MoexError> {
2753        let endpoint = GLOBAL_SECURITIES_ENDPOINT;
2754        let mut endpoint_url = self.endpoint_url(endpoint)?;
2755        {
2756            let mut query = endpoint_url.query_pairs_mut();
2757            query
2758                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
2759                .append_pair(ISS_ONLY_PARAM, "securities")
2760                .append_pair(SECURITIES_COLUMNS_PARAM, SECURITIES_COLUMNS);
2761        }
2762        append_pagination_to_url(&mut endpoint_url, pagination);
2763
2764        let payload = self.fetch_payload(endpoint, endpoint_url).await?;
2765        decode_securities_json_with_endpoint(&payload, endpoint)
2766    }
2767
2768    #[cfg(feature = "news")]
2769    async fn fetch_sitenews_page(
2770        &self,
2771        pagination: Pagination,
2772    ) -> Result<Vec<SiteNews>, MoexError> {
2773        let endpoint = SITENEWS_ENDPOINT;
2774        let mut endpoint_url = self.endpoint_url(endpoint)?;
2775        {
2776            let mut query = endpoint_url.query_pairs_mut();
2777            query
2778                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
2779                .append_pair(ISS_ONLY_PARAM, "sitenews")
2780                .append_pair(SITENEWS_COLUMNS_PARAM, SITENEWS_COLUMNS);
2781        }
2782        append_pagination_to_url(&mut endpoint_url, pagination);
2783
2784        let payload = self.fetch_payload(endpoint, endpoint_url).await?;
2785        decode_sitenews_json_with_endpoint(&payload, endpoint)
2786    }
2787
2788    #[cfg(feature = "news")]
2789    async fn fetch_events_page(&self, pagination: Pagination) -> Result<Vec<Event>, MoexError> {
2790        let endpoint = EVENTS_ENDPOINT;
2791        let mut endpoint_url = self.endpoint_url(endpoint)?;
2792        {
2793            let mut query = endpoint_url.query_pairs_mut();
2794            query
2795                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
2796                .append_pair(ISS_ONLY_PARAM, "events")
2797                .append_pair(EVENTS_COLUMNS_PARAM, EVENTS_COLUMNS);
2798        }
2799        append_pagination_to_url(&mut endpoint_url, pagination);
2800
2801        let payload = self.fetch_payload(endpoint, endpoint_url).await?;
2802        decode_events_json_with_endpoint(&payload, endpoint)
2803    }
2804
2805    async fn fetch_market_securities_page(
2806        &self,
2807        engine: &EngineName,
2808        market: &MarketName,
2809        pagination: Pagination,
2810    ) -> Result<Vec<Security>, MoexError> {
2811        let endpoint = market_securities_endpoint(engine, market);
2812        let mut endpoint_url = self.endpoint_url(endpoint.as_str())?;
2813        {
2814            let mut query = endpoint_url.query_pairs_mut();
2815            query
2816                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
2817                .append_pair(ISS_ONLY_PARAM, "securities")
2818                .append_pair(SECURITIES_COLUMNS_PARAM, SECURITIES_COLUMNS);
2819        }
2820        append_pagination_to_url(&mut endpoint_url, pagination);
2821
2822        let payload = self.fetch_payload(endpoint.as_str(), endpoint_url).await?;
2823        decode_securities_json_with_endpoint(&payload, endpoint.as_str())
2824    }
2825
2826    async fn fetch_market_trades_page(
2827        &self,
2828        engine: &EngineName,
2829        market: &MarketName,
2830        pagination: Pagination,
2831    ) -> Result<Vec<Trade>, MoexError> {
2832        let endpoint = market_trades_endpoint(engine, market);
2833        let mut endpoint_url = self.endpoint_url(endpoint.as_str())?;
2834        {
2835            let mut query = endpoint_url.query_pairs_mut();
2836            query
2837                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
2838                .append_pair(ISS_ONLY_PARAM, "trades")
2839                .append_pair(TRADES_COLUMNS_PARAM, TRADES_COLUMNS);
2840        }
2841        append_pagination_to_url(&mut endpoint_url, pagination);
2842
2843        let payload = self.fetch_payload(endpoint.as_str(), endpoint_url).await?;
2844        decode_trades_json_with_endpoint(&payload, endpoint.as_str())
2845    }
2846
2847    async fn fetch_secstats_page(
2848        &self,
2849        engine: &EngineName,
2850        market: &MarketName,
2851        pagination: Pagination,
2852    ) -> Result<Vec<SecStat>, MoexError> {
2853        let endpoint = secstats_endpoint(engine, market);
2854        let mut endpoint_url = self.endpoint_url(endpoint.as_str())?;
2855        {
2856            let mut query = endpoint_url.query_pairs_mut();
2857            query
2858                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
2859                .append_pair(ISS_ONLY_PARAM, "secstats")
2860                .append_pair(SECSTATS_COLUMNS_PARAM, SECSTATS_COLUMNS);
2861        }
2862        append_pagination_to_url(&mut endpoint_url, pagination);
2863
2864        let payload = self.fetch_payload(endpoint.as_str(), endpoint_url).await?;
2865        decode_secstats_json_with_endpoint(&payload, endpoint.as_str())
2866    }
2867
2868    async fn fetch_candles_page(
2869        &self,
2870        engine: &EngineName,
2871        market: &MarketName,
2872        board: &BoardId,
2873        security: &SecId,
2874        query: CandleQuery,
2875        pagination: Pagination,
2876    ) -> Result<Vec<Candle>, MoexError> {
2877        let endpoint = candles_endpoint(engine, market, board, security);
2878        let mut endpoint_url = self.endpoint_url(endpoint.as_str())?;
2879        {
2880            let mut query_pairs = endpoint_url.query_pairs_mut();
2881            query_pairs
2882                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
2883                .append_pair(ISS_ONLY_PARAM, "candles")
2884                .append_pair(CANDLES_COLUMNS_PARAM, CANDLES_COLUMNS);
2885        }
2886        append_candle_query_to_url(&mut endpoint_url, query);
2887        append_pagination_to_url(&mut endpoint_url, pagination);
2888
2889        let payload = self.fetch_payload(endpoint.as_str(), endpoint_url).await?;
2890        decode_candles_json_with_endpoint(&payload, endpoint.as_str())
2891    }
2892
2893    async fn fetch_trades_page(
2894        &self,
2895        engine: &EngineName,
2896        market: &MarketName,
2897        board: &BoardId,
2898        security: &SecId,
2899        pagination: Pagination,
2900    ) -> Result<Vec<Trade>, MoexError> {
2901        let endpoint = trades_endpoint(engine, market, board, security);
2902        let mut endpoint_url = self.endpoint_url(endpoint.as_str())?;
2903        {
2904            let mut query = endpoint_url.query_pairs_mut();
2905            query
2906                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
2907                .append_pair(ISS_ONLY_PARAM, "trades")
2908                .append_pair(TRADES_COLUMNS_PARAM, TRADES_COLUMNS);
2909        }
2910        append_pagination_to_url(&mut endpoint_url, pagination);
2911
2912        let payload = self.fetch_payload(endpoint.as_str(), endpoint_url).await?;
2913        decode_trades_json_with_endpoint(&payload, endpoint.as_str())
2914    }
2915
2916    #[cfg(feature = "history")]
2917    async fn fetch_history_page(
2918        &self,
2919        engine: &EngineName,
2920        market: &MarketName,
2921        board: &BoardId,
2922        security: &SecId,
2923        pagination: Pagination,
2924    ) -> Result<Vec<HistoryRecord>, MoexError> {
2925        let endpoint = history_endpoint(engine, market, board, security);
2926        let mut endpoint_url = self.endpoint_url(endpoint.as_str())?;
2927        {
2928            let mut query = endpoint_url.query_pairs_mut();
2929            query
2930                .append_pair(ISS_META_PARAM, metadata_value(self.metadata))
2931                .append_pair(ISS_ONLY_PARAM, "history")
2932                .append_pair(HISTORY_COLUMNS_PARAM, HISTORY_COLUMNS);
2933        }
2934        append_pagination_to_url(&mut endpoint_url, pagination);
2935
2936        let payload = self.fetch_payload(endpoint.as_str(), endpoint_url).await?;
2937        decode_history_json_with_endpoint(&payload, endpoint.as_str())
2938    }
2939
2940    fn endpoint_url(&self, endpoint: &str) -> Result<Url, MoexError> {
2941        self.base_url
2942            .join(endpoint)
2943            .map_err(|source| MoexError::EndpointUrl {
2944                endpoint: endpoint.to_owned().into_boxed_str(),
2945                reason: source.to_string(),
2946            })
2947    }
2948
2949    async fn get_payload(
2950        &self,
2951        endpoint: &str,
2952        query_params: &[(&'static str, &'static str)],
2953    ) -> Result<String, MoexError> {
2954        let mut endpoint_url = self.endpoint_url(endpoint)?;
2955        {
2956            let mut url_query = endpoint_url.query_pairs_mut();
2957            for (key, value) in query_params {
2958                url_query.append_pair(key, value);
2959            }
2960        }
2961        self.fetch_payload(endpoint, endpoint_url).await
2962    }
2963
2964    async fn fetch_payload(&self, endpoint: &str, endpoint_url: Url) -> Result<String, MoexError> {
2965        self.wait_for_rate_limit().await;
2966        let response = self
2967            .client
2968            .get(endpoint_url)
2969            .send()
2970            .await
2971            .map_err(|source| MoexError::Request {
2972                endpoint: endpoint.to_owned().into_boxed_str(),
2973                source,
2974            })?;
2975        let status = response.status();
2976
2977        let content_type = response
2978            .headers()
2979            .get(reqwest::header::CONTENT_TYPE)
2980            .and_then(|value| value.to_str().ok())
2981            .map(|value| value.to_owned().into_boxed_str());
2982
2983        let payload = response
2984            .text()
2985            .await
2986            .map_err(|source| MoexError::ReadBody {
2987                endpoint: endpoint.to_owned().into_boxed_str(),
2988                source,
2989            })?;
2990
2991        if !status.is_success() {
2992            return Err(MoexError::HttpStatus {
2993                endpoint: endpoint.to_owned().into_boxed_str(),
2994                status,
2995                content_type,
2996                body_prefix: truncate_prefix(&payload, NON_JSON_BODY_PREFIX_CHARS),
2997            });
2998        }
2999
3000        if !looks_like_json_payload(content_type.as_deref(), &payload) {
3001            return Err(MoexError::NonJsonPayload {
3002                endpoint: endpoint.to_owned().into_boxed_str(),
3003                content_type,
3004                body_prefix: truncate_prefix(&payload, NON_JSON_BODY_PREFIX_CHARS),
3005            });
3006        }
3007
3008        Ok(payload)
3009    }
3010
3011    async fn wait_for_rate_limit(&self) {
3012        let Some(rate_limit) = &self.rate_limit else {
3013            return;
3014        };
3015        let delay = reserve_rate_limit_delay(&rate_limit.limiter);
3016        if !delay.is_zero() {
3017            (rate_limit.sleep)(delay).await;
3018        }
3019    }
3020}
3021
3022#[cfg(feature = "blocking")]
3023impl<'a> RawIssRequestBuilder<'a> {
3024    /// Установить endpoint-path относительно `/iss/`.
3025    ///
3026    /// Допускаются формы:
3027    /// - `engines`
3028    /// - `engines.json`
3029    /// - `/iss/engines`
3030    pub fn path(mut self, path: impl Into<String>) -> Self {
3031        self.path = Some(path.into().into_boxed_str());
3032        self
3033    }
3034
3035    /// Добавить query-параметр.
3036    pub fn param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
3037        self.query
3038            .push((key.into().into_boxed_str(), value.into().into_boxed_str()));
3039        self
3040    }
3041
3042    /// Добавить параметр `iss.only`.
3043    pub fn only(self, tables: impl Into<String>) -> Self {
3044        self.param(ISS_ONLY_PARAM, tables)
3045    }
3046
3047    /// Добавить параметр `<table>.columns`.
3048    pub fn columns(self, table: impl Into<String>, columns: impl Into<String>) -> Self {
3049        let mut key = table.into();
3050        key.push_str(".columns");
3051        self.param(key, columns)
3052    }
3053
3054    /// Явно задать `iss.meta` для текущего raw-запроса.
3055    pub fn metadata(self, metadata: IssToggle) -> Self {
3056        self.param(ISS_META_PARAM, metadata.as_query_value())
3057    }
3058
3059    /// Добавить параметр `iss.data`.
3060    pub fn data(self, data: IssToggle) -> Self {
3061        self.param(ISS_DATA_PARAM, data.as_query_value())
3062    }
3063
3064    /// Добавить параметр `iss.json`.
3065    pub fn json(self, json: impl Into<String>) -> Self {
3066        self.param(ISS_JSON_PARAM, json)
3067    }
3068
3069    /// Добавить параметр `iss.version`.
3070    pub fn version(self, version: IssToggle) -> Self {
3071        self.param(ISS_VERSION_PARAM, version.as_query_value())
3072    }
3073
3074    /// Применить пакет системных `iss.*`-опций.
3075    pub fn options(mut self, options: IssRequestOptions) -> Self {
3076        apply_iss_request_options(&mut self.query, options);
3077        self
3078    }
3079
3080    /// Выполнить raw-запрос и вернуть полный HTTP-ответ.
3081    ///
3082    /// В отличие от `send_payload`, метод не проверяет `2xx` и JSON-формат.
3083    pub fn send_response(self) -> Result<RawIssResponse, MoexError> {
3084        let (_, response) = self.execute_response()?;
3085        Ok(response)
3086    }
3087
3088    /// Выполнить raw-запрос и вернуть тело ответа как строку.
3089    pub fn send_payload(self) -> Result<String, MoexError> {
3090        let (_, payload) = self.execute()?;
3091        Ok(payload)
3092    }
3093
3094    /// Выполнить raw-запрос и декодировать JSON в пользовательский тип.
3095    pub fn send_json<T>(self) -> Result<T, MoexError>
3096    where
3097        T: serde::de::DeserializeOwned,
3098    {
3099        let (endpoint, payload) = self.execute()?;
3100        serde_json::from_str(&payload).map_err(|source| MoexError::Decode { endpoint, source })
3101    }
3102
3103    /// Выполнить raw-запрос и декодировать строки выбранной ISS-таблицы в пользовательский тип.
3104    pub fn send_table<T>(self, table: impl Into<String>) -> Result<Vec<T>, MoexError>
3105    where
3106        T: serde::de::DeserializeOwned,
3107    {
3108        let table = table.into();
3109        let (endpoint, payload) = self.execute()?;
3110        decode_raw_table_rows_json_with_endpoint(&payload, endpoint.as_ref(), table.as_str())
3111    }
3112
3113    fn execute(self) -> Result<(Box<str>, String), MoexError> {
3114        let (endpoint, endpoint_url) = self.build_request()?;
3115        let payload = self.client.fetch_payload(&endpoint, endpoint_url)?;
3116        Ok((endpoint, payload))
3117    }
3118
3119    fn execute_response(self) -> Result<(Box<str>, RawIssResponse), MoexError> {
3120        let (endpoint, endpoint_url) = self.build_request()?;
3121        self.client.wait_for_rate_limit();
3122        let response = self
3123            .client
3124            .client
3125            .get(endpoint_url)
3126            .send()
3127            .map_err(|source| MoexError::Request {
3128                endpoint: endpoint.clone(),
3129                source,
3130            })?;
3131        let status = response.status();
3132        let headers = response.headers().clone();
3133        let body = response.text().map_err(|source| MoexError::ReadBody {
3134            endpoint: endpoint.clone(),
3135            source,
3136        })?;
3137        Ok((endpoint, RawIssResponse::new(status, headers, body)))
3138    }
3139
3140    fn build_request(&self) -> Result<(Box<str>, Url), MoexError> {
3141        let endpoint = normalize_raw_endpoint_path(self.path.as_deref())?;
3142        let mut endpoint_url = self.client.endpoint_url(&endpoint)?;
3143        let has_meta = self
3144            .query
3145            .iter()
3146            .any(|(key, _)| key.as_ref() == ISS_META_PARAM);
3147        {
3148            let mut url_query = endpoint_url.query_pairs_mut();
3149            if !has_meta {
3150                url_query.append_pair(ISS_META_PARAM, metadata_value(self.client.metadata));
3151            }
3152            for (key, value) in &self.query {
3153                url_query.append_pair(key, value);
3154            }
3155        }
3156        Ok((endpoint, endpoint_url))
3157    }
3158}
3159
3160#[cfg(feature = "async")]
3161impl<'a> AsyncRawIssRequestBuilder<'a> {
3162    /// Установить endpoint-path относительно `/iss/`.
3163    ///
3164    /// Допускаются формы:
3165    /// - `engines`
3166    /// - `engines.json`
3167    /// - `/iss/engines`
3168    pub fn path(mut self, path: impl Into<String>) -> Self {
3169        self.path = Some(path.into().into_boxed_str());
3170        self
3171    }
3172
3173    /// Добавить query-параметр.
3174    pub fn param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
3175        self.query
3176            .push((key.into().into_boxed_str(), value.into().into_boxed_str()));
3177        self
3178    }
3179
3180    /// Добавить параметр `iss.only`.
3181    pub fn only(self, tables: impl Into<String>) -> Self {
3182        self.param(ISS_ONLY_PARAM, tables)
3183    }
3184
3185    /// Добавить параметр `<table>.columns`.
3186    pub fn columns(self, table: impl Into<String>, columns: impl Into<String>) -> Self {
3187        let mut key = table.into();
3188        key.push_str(".columns");
3189        self.param(key, columns)
3190    }
3191
3192    /// Явно задать `iss.meta` для текущего raw-запроса.
3193    pub fn metadata(self, metadata: IssToggle) -> Self {
3194        self.param(ISS_META_PARAM, metadata.as_query_value())
3195    }
3196
3197    /// Добавить параметр `iss.data`.
3198    pub fn data(self, data: IssToggle) -> Self {
3199        self.param(ISS_DATA_PARAM, data.as_query_value())
3200    }
3201
3202    /// Добавить параметр `iss.json`.
3203    pub fn json(self, json: impl Into<String>) -> Self {
3204        self.param(ISS_JSON_PARAM, json)
3205    }
3206
3207    /// Добавить параметр `iss.version`.
3208    pub fn version(self, version: IssToggle) -> Self {
3209        self.param(ISS_VERSION_PARAM, version.as_query_value())
3210    }
3211
3212    /// Применить пакет системных `iss.*`-опций.
3213    pub fn options(mut self, options: IssRequestOptions) -> Self {
3214        apply_iss_request_options(&mut self.query, options);
3215        self
3216    }
3217
3218    /// Выполнить raw-запрос и вернуть полный HTTP-ответ.
3219    ///
3220    /// В отличие от `send_payload`, метод не проверяет `2xx` и JSON-формат.
3221    pub async fn send_response(self) -> Result<RawIssResponse, MoexError> {
3222        let (_, response) = self.execute_response().await?;
3223        Ok(response)
3224    }
3225
3226    /// Выполнить raw-запрос и вернуть тело ответа как строку.
3227    pub async fn send_payload(self) -> Result<String, MoexError> {
3228        let (_, payload) = self.execute().await?;
3229        Ok(payload)
3230    }
3231
3232    /// Выполнить raw-запрос и декодировать JSON в пользовательский тип.
3233    pub async fn send_json<T>(self) -> Result<T, MoexError>
3234    where
3235        T: serde::de::DeserializeOwned,
3236    {
3237        let (endpoint, payload) = self.execute().await?;
3238        serde_json::from_str(&payload).map_err(|source| MoexError::Decode { endpoint, source })
3239    }
3240
3241    /// Выполнить raw-запрос и декодировать строки выбранной ISS-таблицы в пользовательский тип.
3242    pub async fn send_table<T>(self, table: impl Into<String>) -> Result<Vec<T>, MoexError>
3243    where
3244        T: serde::de::DeserializeOwned,
3245    {
3246        let table = table.into();
3247        let (endpoint, payload) = self.execute().await?;
3248        decode_raw_table_rows_json_with_endpoint(&payload, endpoint.as_ref(), table.as_str())
3249    }
3250
3251    async fn execute(self) -> Result<(Box<str>, String), MoexError> {
3252        let (endpoint, endpoint_url) = self.build_request()?;
3253        let payload = self.client.fetch_payload(&endpoint, endpoint_url).await?;
3254        Ok((endpoint, payload))
3255    }
3256
3257    async fn execute_response(self) -> Result<(Box<str>, RawIssResponse), MoexError> {
3258        let (endpoint, endpoint_url) = self.build_request()?;
3259        self.client.wait_for_rate_limit().await;
3260        let response = self
3261            .client
3262            .client
3263            .get(endpoint_url)
3264            .send()
3265            .await
3266            .map_err(|source| MoexError::Request {
3267                endpoint: endpoint.clone(),
3268                source,
3269            })?;
3270        let status = response.status();
3271        let headers = response.headers().clone();
3272        let body = response
3273            .text()
3274            .await
3275            .map_err(|source| MoexError::ReadBody {
3276                endpoint: endpoint.clone(),
3277                source,
3278            })?;
3279        Ok((endpoint, RawIssResponse::new(status, headers, body)))
3280    }
3281
3282    fn build_request(&self) -> Result<(Box<str>, Url), MoexError> {
3283        let endpoint = normalize_raw_endpoint_path(self.path.as_deref())?;
3284        let mut endpoint_url = self.client.endpoint_url(&endpoint)?;
3285        let has_meta = self
3286            .query
3287            .iter()
3288            .any(|(key, _)| key.as_ref() == ISS_META_PARAM);
3289        {
3290            let mut url_query = endpoint_url.query_pairs_mut();
3291            if !has_meta {
3292                url_query.append_pair(ISS_META_PARAM, metadata_value(self.client.metadata));
3293            }
3294            for (key, value) in &self.query {
3295                url_query.append_pair(key, value);
3296            }
3297        }
3298        Ok((endpoint, endpoint_url))
3299    }
3300}
3301
3302#[cfg(feature = "blocking")]
3303fn next_page_blocking<T, K, F, G>(
3304    pagination: &mut PaginationTracker<K>,
3305    fetch_page: F,
3306    first_key_of: G,
3307) -> Result<Option<Vec<T>>, MoexError>
3308where
3309    K: Eq,
3310    F: FnOnce(Pagination) -> Result<Vec<T>, MoexError>,
3311    G: Fn(&T) -> K,
3312{
3313    let Some(paging) = pagination.next_page_request() else {
3314        return Ok(None);
3315    };
3316    let page = fetch_page(paging)?;
3317    let first_key_on_page = page.first().map(first_key_of);
3318    match pagination.advance(page.len(), first_key_on_page)? {
3319        PaginationAdvance::YieldPage => Ok(Some(page)),
3320        PaginationAdvance::EndOfPages => Ok(None),
3321    }
3322}
3323
3324#[cfg(feature = "async")]
3325async fn next_page_async<T, K, F, Fut, G>(
3326    pagination: &mut PaginationTracker<K>,
3327    fetch_page: F,
3328    first_key_of: G,
3329) -> Result<Option<Vec<T>>, MoexError>
3330where
3331    K: Eq,
3332    F: FnOnce(Pagination) -> Fut,
3333    Fut: std::future::Future<Output = Result<Vec<T>, MoexError>>,
3334    G: Fn(&T) -> K,
3335{
3336    let Some(paging) = pagination.next_page_request() else {
3337        return Ok(None);
3338    };
3339    let page = fetch_page(paging).await?;
3340    let first_key_on_page = page.first().map(first_key_of);
3341    match pagination.advance(page.len(), first_key_on_page)? {
3342        PaginationAdvance::YieldPage => Ok(Some(page)),
3343        PaginationAdvance::EndOfPages => Ok(None),
3344    }
3345}
3346
3347#[cfg(feature = "blocking")]
3348fn collect_pages_blocking<T, F>(mut next_page: F) -> Result<Vec<T>, MoexError>
3349where
3350    F: FnMut() -> Result<Option<Vec<T>>, MoexError>,
3351{
3352    let mut items = Vec::new();
3353    while let Some(page) = next_page()? {
3354        items.extend(page);
3355    }
3356    Ok(items)
3357}
3358
3359#[cfg(feature = "async")]
3360impl<'a> AsyncIndexAnalyticsPages<'a> {
3361    /// Получить следующую страницу `index_analytics`.
3362    pub async fn next_page(&mut self) -> Result<Option<Vec<IndexAnalytics>>, MoexError> {
3363        next_page_async(
3364            &mut self.pagination,
3365            |pagination| {
3366                self.client
3367                    .fetch_index_analytics_page(self.indexid, pagination)
3368            },
3369            |item| (item.trade_session_date(), item.secid().clone()),
3370        )
3371        .await
3372    }
3373
3374    /// Собрать все страницы `index_analytics` в один `Vec`.
3375    pub async fn try_collect(mut self) -> Result<Vec<IndexAnalytics>, MoexError> {
3376        {
3377            let mut items = Vec::new();
3378            while let Some(page) = self.next_page().await? {
3379                items.extend(page);
3380            }
3381            Ok(items)
3382        }
3383    }
3384
3385    /// Алиас для [`Self::try_collect`].
3386    pub async fn all(self) -> Result<Vec<IndexAnalytics>, MoexError> {
3387        self.try_collect().await
3388    }
3389}
3390
3391#[cfg(feature = "async")]
3392impl<'a> AsyncSecuritiesPages<'a> {
3393    /// Получить следующую страницу `securities`.
3394    pub async fn next_page(&mut self) -> Result<Option<Vec<Security>>, MoexError> {
3395        next_page_async(
3396            &mut self.pagination,
3397            |pagination| {
3398                self.client
3399                    .fetch_securities_page(self.engine, self.market, self.board, pagination)
3400            },
3401            |item| item.secid().clone(),
3402        )
3403        .await
3404    }
3405
3406    /// Собрать все страницы `securities` в один `Vec`.
3407    pub async fn try_collect(mut self) -> Result<Vec<Security>, MoexError> {
3408        {
3409            let mut items = Vec::new();
3410            while let Some(page) = self.next_page().await? {
3411                items.extend(page);
3412            }
3413            Ok(items)
3414        }
3415    }
3416
3417    /// Алиас для [`Self::try_collect`].
3418    pub async fn all(self) -> Result<Vec<Security>, MoexError> {
3419        self.try_collect().await
3420    }
3421}
3422
3423#[cfg(feature = "async")]
3424impl<'a> AsyncGlobalSecuritiesPages<'a> {
3425    /// Получить следующую страницу глобального `securities`.
3426    pub async fn next_page(&mut self) -> Result<Option<Vec<Security>>, MoexError> {
3427        next_page_async(
3428            &mut self.pagination,
3429            |pagination| self.client.fetch_global_securities_page(pagination),
3430            |item| item.secid().clone(),
3431        )
3432        .await
3433    }
3434
3435    /// Собрать все страницы глобального `securities` в один `Vec`.
3436    pub async fn try_collect(mut self) -> Result<Vec<Security>, MoexError> {
3437        {
3438            let mut items = Vec::new();
3439            while let Some(page) = self.next_page().await? {
3440                items.extend(page);
3441            }
3442            Ok(items)
3443        }
3444    }
3445
3446    /// Алиас для [`Self::try_collect`].
3447    pub async fn all(self) -> Result<Vec<Security>, MoexError> {
3448        self.try_collect().await
3449    }
3450}
3451
3452#[cfg(all(feature = "async", feature = "news"))]
3453impl<'a> AsyncSiteNewsPages<'a> {
3454    /// Получить следующую страницу `sitenews`.
3455    pub async fn next_page(&mut self) -> Result<Option<Vec<SiteNews>>, MoexError> {
3456        next_page_async(
3457            &mut self.pagination,
3458            |pagination| self.client.fetch_sitenews_page(pagination),
3459            SiteNews::id,
3460        )
3461        .await
3462    }
3463
3464    /// Собрать все страницы `sitenews` в один `Vec`.
3465    pub async fn try_collect(mut self) -> Result<Vec<SiteNews>, MoexError> {
3466        {
3467            let mut items = Vec::new();
3468            while let Some(page) = self.next_page().await? {
3469                items.extend(page);
3470            }
3471            Ok(items)
3472        }
3473    }
3474
3475    /// Алиас для [`Self::try_collect`].
3476    pub async fn all(self) -> Result<Vec<SiteNews>, MoexError> {
3477        self.try_collect().await
3478    }
3479}
3480
3481#[cfg(all(feature = "async", feature = "news"))]
3482impl<'a> AsyncEventsPages<'a> {
3483    /// Получить следующую страницу `events`.
3484    pub async fn next_page(&mut self) -> Result<Option<Vec<Event>>, MoexError> {
3485        next_page_async(
3486            &mut self.pagination,
3487            |pagination| self.client.fetch_events_page(pagination),
3488            Event::id,
3489        )
3490        .await
3491    }
3492
3493    /// Собрать все страницы `events` в один `Vec`.
3494    pub async fn try_collect(mut self) -> Result<Vec<Event>, MoexError> {
3495        {
3496            let mut items = Vec::new();
3497            while let Some(page) = self.next_page().await? {
3498                items.extend(page);
3499            }
3500            Ok(items)
3501        }
3502    }
3503
3504    /// Алиас для [`Self::try_collect`].
3505    pub async fn all(self) -> Result<Vec<Event>, MoexError> {
3506        self.try_collect().await
3507    }
3508}
3509
3510#[cfg(feature = "async")]
3511impl<'a> AsyncMarketSecuritiesPages<'a> {
3512    /// Получить следующую страницу market-level `securities`.
3513    pub async fn next_page(&mut self) -> Result<Option<Vec<Security>>, MoexError> {
3514        next_page_async(
3515            &mut self.pagination,
3516            |pagination| {
3517                self.client
3518                    .fetch_market_securities_page(self.engine, self.market, pagination)
3519            },
3520            |item| item.secid().clone(),
3521        )
3522        .await
3523    }
3524
3525    /// Собрать все страницы market-level `securities` в один `Vec`.
3526    pub async fn try_collect(mut self) -> Result<Vec<Security>, MoexError> {
3527        {
3528            let mut items = Vec::new();
3529            while let Some(page) = self.next_page().await? {
3530                items.extend(page);
3531            }
3532            Ok(items)
3533        }
3534    }
3535
3536    /// Алиас для [`Self::try_collect`].
3537    pub async fn all(self) -> Result<Vec<Security>, MoexError> {
3538        self.try_collect().await
3539    }
3540}
3541
3542#[cfg(feature = "async")]
3543impl<'a> AsyncMarketTradesPages<'a> {
3544    /// Получить следующую страницу market-level `trades`.
3545    pub async fn next_page(&mut self) -> Result<Option<Vec<Trade>>, MoexError> {
3546        next_page_async(
3547            &mut self.pagination,
3548            |pagination| {
3549                self.client
3550                    .fetch_market_trades_page(self.engine, self.market, pagination)
3551            },
3552            Trade::tradeno,
3553        )
3554        .await
3555    }
3556
3557    /// Собрать все страницы market-level `trades` в один `Vec`.
3558    pub async fn try_collect(mut self) -> Result<Vec<Trade>, MoexError> {
3559        {
3560            let mut items = Vec::new();
3561            while let Some(page) = self.next_page().await? {
3562                items.extend(page);
3563            }
3564            Ok(items)
3565        }
3566    }
3567
3568    /// Алиас для [`Self::try_collect`].
3569    pub async fn all(self) -> Result<Vec<Trade>, MoexError> {
3570        self.try_collect().await
3571    }
3572}
3573
3574#[cfg(feature = "async")]
3575impl<'a> AsyncTradesPages<'a> {
3576    /// Получить следующую страницу `trades`.
3577    pub async fn next_page(&mut self) -> Result<Option<Vec<Trade>>, MoexError> {
3578        next_page_async(
3579            &mut self.pagination,
3580            |pagination| {
3581                self.client.fetch_trades_page(
3582                    self.engine,
3583                    self.market,
3584                    self.board,
3585                    self.security,
3586                    pagination,
3587                )
3588            },
3589            Trade::tradeno,
3590        )
3591        .await
3592    }
3593
3594    /// Собрать все страницы `trades` в один `Vec`.
3595    pub async fn try_collect(mut self) -> Result<Vec<Trade>, MoexError> {
3596        {
3597            let mut items = Vec::new();
3598            while let Some(page) = self.next_page().await? {
3599                items.extend(page);
3600            }
3601            Ok(items)
3602        }
3603    }
3604
3605    /// Алиас для [`Self::try_collect`].
3606    pub async fn all(self) -> Result<Vec<Trade>, MoexError> {
3607        self.try_collect().await
3608    }
3609}
3610
3611#[cfg(all(feature = "async", feature = "history"))]
3612impl<'a> AsyncHistoryPages<'a> {
3613    /// Получить следующую страницу `history`.
3614    pub async fn next_page(&mut self) -> Result<Option<Vec<HistoryRecord>>, MoexError> {
3615        next_page_async(
3616            &mut self.pagination,
3617            |pagination| {
3618                self.client.fetch_history_page(
3619                    self.engine,
3620                    self.market,
3621                    self.board,
3622                    self.security,
3623                    pagination,
3624                )
3625            },
3626            HistoryRecord::tradedate,
3627        )
3628        .await
3629    }
3630
3631    /// Собрать все страницы `history` в один `Vec`.
3632    pub async fn try_collect(mut self) -> Result<Vec<HistoryRecord>, MoexError> {
3633        {
3634            let mut items = Vec::new();
3635            while let Some(page) = self.next_page().await? {
3636                items.extend(page);
3637            }
3638            Ok(items)
3639        }
3640    }
3641
3642    /// Алиас для [`Self::try_collect`].
3643    pub async fn all(self) -> Result<Vec<HistoryRecord>, MoexError> {
3644        self.try_collect().await
3645    }
3646}
3647
3648#[cfg(feature = "async")]
3649impl<'a> AsyncSecStatsPages<'a> {
3650    /// Получить следующую страницу `secstats`.
3651    pub async fn next_page(&mut self) -> Result<Option<Vec<SecStat>>, MoexError> {
3652        next_page_async(
3653            &mut self.pagination,
3654            |pagination| {
3655                self.client
3656                    .fetch_secstats_page(self.engine, self.market, pagination)
3657            },
3658            |item| (item.secid().clone(), item.boardid().clone()),
3659        )
3660        .await
3661    }
3662
3663    /// Собрать все страницы `secstats` в один `Vec`.
3664    pub async fn try_collect(mut self) -> Result<Vec<SecStat>, MoexError> {
3665        {
3666            let mut items = Vec::new();
3667            while let Some(page) = self.next_page().await? {
3668                items.extend(page);
3669            }
3670            Ok(items)
3671        }
3672    }
3673
3674    /// Алиас для [`Self::try_collect`].
3675    pub async fn all(self) -> Result<Vec<SecStat>, MoexError> {
3676        self.try_collect().await
3677    }
3678}
3679
3680#[cfg(feature = "async")]
3681impl<'a> AsyncCandlesPages<'a> {
3682    /// Получить следующую страницу `candles`.
3683    pub async fn next_page(&mut self) -> Result<Option<Vec<Candle>>, MoexError> {
3684        next_page_async(
3685            &mut self.pagination,
3686            |pagination| {
3687                self.client.fetch_candles_page(
3688                    self.engine,
3689                    self.market,
3690                    self.board,
3691                    self.security,
3692                    self.query,
3693                    pagination,
3694                )
3695            },
3696            Candle::begin,
3697        )
3698        .await
3699    }
3700
3701    /// Собрать все страницы `candles` в один `Vec`.
3702    pub async fn try_collect(mut self) -> Result<Vec<Candle>, MoexError> {
3703        {
3704            let mut items = Vec::new();
3705            while let Some(page) = self.next_page().await? {
3706                items.extend(page);
3707            }
3708            Ok(items)
3709        }
3710    }
3711
3712    /// Алиас для [`Self::try_collect`].
3713    pub async fn all(self) -> Result<Vec<Candle>, MoexError> {
3714        self.try_collect().await
3715    }
3716}
3717
3718#[cfg(feature = "async")]
3719impl<'a> AsyncOwnedIndexScope<'a> {
3720    /// Идентификатор индекса текущего async owning-scope.
3721    pub fn indexid(&self) -> &IndexId {
3722        &self.indexid
3723    }
3724
3725    /// Получить состав индекса (`analytics`) по текущему async owning-scope.
3726    pub async fn analytics(
3727        &self,
3728        page_request: PageRequest,
3729    ) -> Result<Vec<IndexAnalytics>, MoexError> {
3730        self.client
3731            .index_analytics_query(&self.indexid, page_request)
3732            .await
3733    }
3734
3735    /// Создать асинхронный ленивый paginator `analytics` для текущего индекса.
3736    pub fn analytics_pages(&self, page_limit: NonZeroU32) -> AsyncIndexAnalyticsPages<'_> {
3737        self.client.index_analytics_pages(&self.indexid, page_limit)
3738    }
3739}
3740
3741#[cfg(feature = "async")]
3742impl<'a> AsyncOwnedEngineScope<'a> {
3743    /// Имя торгового движка текущего async owning-scope.
3744    pub fn engine(&self) -> &EngineName {
3745        &self.engine
3746    }
3747
3748    /// Получить доступные рынки (`markets`) для текущего движка.
3749    pub async fn markets(&self) -> Result<Vec<Market>, MoexError> {
3750        self.client.markets(&self.engine).await
3751    }
3752
3753    /// Получить обороты (`turnovers`) для текущего движка.
3754    pub async fn turnovers(&self) -> Result<Vec<Turnover>, MoexError> {
3755        self.client.engine_turnovers(&self.engine).await
3756    }
3757
3758    /// Зафиксировать рынок внутри текущего `engine`.
3759    pub fn market<M>(self, market: M) -> Result<AsyncOwnedMarketScope<'a>, ParseMarketNameError>
3760    where
3761        M: TryInto<MarketName>,
3762        M::Error: Into<ParseMarketNameError>,
3763    {
3764        let market = market.try_into().map_err(Into::into)?;
3765        Ok(AsyncOwnedMarketScope {
3766            client: self.client,
3767            engine: self.engine,
3768            market,
3769        })
3770    }
3771
3772    /// Shortcut для часто используемого рынка `shares`.
3773    pub fn shares(self) -> Result<AsyncOwnedMarketScope<'a>, ParseMarketNameError> {
3774        self.market("shares")
3775    }
3776}
3777
3778#[cfg(feature = "async")]
3779impl<'a> AsyncOwnedMarketScope<'a> {
3780    /// Имя торгового движка текущего async owning-scope.
3781    pub fn engine(&self) -> &EngineName {
3782        &self.engine
3783    }
3784
3785    /// Имя рынка текущего async owning-scope.
3786    pub fn market(&self) -> &MarketName {
3787        &self.market
3788    }
3789
3790    /// Получить режимы торгов (`boards`) для текущего рынка.
3791    pub async fn boards(&self) -> Result<Vec<Board>, MoexError> {
3792        self.client.boards(&self.engine, &self.market).await
3793    }
3794
3795    /// Получить инструменты (`securities`) на уровне текущего рынка.
3796    pub async fn securities(&self, page_request: PageRequest) -> Result<Vec<Security>, MoexError> {
3797        self.client
3798            .market_securities_query(&self.engine, &self.market, page_request)
3799            .await
3800    }
3801
3802    /// Создать асинхронный ленивый paginator market-level `securities`.
3803    pub fn securities_pages(&self, page_limit: NonZeroU32) -> AsyncMarketSecuritiesPages<'_> {
3804        self.client
3805            .market_securities_pages(&self.engine, &self.market, page_limit)
3806    }
3807
3808    /// Получить market-level стакан (`orderbook`) для текущего рынка.
3809    pub async fn orderbook(&self) -> Result<Vec<OrderbookLevel>, MoexError> {
3810        self.client
3811            .market_orderbook(&self.engine, &self.market)
3812            .await
3813    }
3814
3815    /// Получить market-level сделки (`trades`) для текущего рынка.
3816    pub async fn trades(&self, page_request: PageRequest) -> Result<Vec<Trade>, MoexError> {
3817        self.client
3818            .market_trades_query(&self.engine, &self.market, page_request)
3819            .await
3820    }
3821
3822    /// Создать асинхронный ленивый paginator market-level `trades`.
3823    pub fn trades_pages(&self, page_limit: NonZeroU32) -> AsyncMarketTradesPages<'_> {
3824        self.client
3825            .market_trades_pages(&self.engine, &self.market, page_limit)
3826    }
3827
3828    /// Получить `secstats` для текущего рынка.
3829    pub async fn secstats(&self, page_request: PageRequest) -> Result<Vec<SecStat>, MoexError> {
3830        self.client
3831            .secstats_query(&self.engine, &self.market, page_request)
3832            .await
3833    }
3834
3835    /// Создать асинхронный ленивый paginator `secstats`.
3836    pub fn secstats_pages(&self, page_limit: NonZeroU32) -> AsyncSecStatsPages<'_> {
3837        self.client
3838            .secstats_pages(&self.engine, &self.market, page_limit)
3839    }
3840
3841    /// Получить доступные границы свечей (`candleborders`) по инструменту.
3842    pub async fn candle_borders(&self, security: &SecId) -> Result<Vec<CandleBorder>, MoexError> {
3843        self.client
3844            .candle_borders(&self.engine, &self.market, security)
3845            .await
3846    }
3847
3848    /// Зафиксировать инструмент в рамках текущего `engine/market`.
3849    pub fn security<S>(
3850        self,
3851        security: S,
3852    ) -> Result<AsyncOwnedMarketSecurityScope<'a>, ParseSecIdError>
3853    where
3854        S: TryInto<SecId>,
3855        S::Error: Into<ParseSecIdError>,
3856    {
3857        let security = security.try_into().map_err(Into::into)?;
3858        Ok(AsyncOwnedMarketSecurityScope {
3859            client: self.client,
3860            engine: self.engine,
3861            market: self.market,
3862            security,
3863        })
3864    }
3865
3866    /// Зафиксировать `board` внутри текущего `engine/market`.
3867    pub fn board<B>(self, board: B) -> Result<AsyncOwnedBoardScope<'a>, ParseBoardIdError>
3868    where
3869        B: TryInto<BoardId>,
3870        B::Error: Into<ParseBoardIdError>,
3871    {
3872        let board = board.try_into().map_err(Into::into)?;
3873        Ok(AsyncOwnedBoardScope {
3874            client: self.client,
3875            engine: self.engine,
3876            market: self.market,
3877            board,
3878        })
3879    }
3880}
3881
3882#[cfg(feature = "async")]
3883impl<'a> AsyncOwnedBoardScope<'a> {
3884    /// Имя торгового движка текущего async owning-scope.
3885    pub fn engine(&self) -> &EngineName {
3886        &self.engine
3887    }
3888
3889    /// Имя рынка текущего async owning-scope.
3890    pub fn market(&self) -> &MarketName {
3891        &self.market
3892    }
3893
3894    /// Идентификатор режима торгов текущего async owning-scope.
3895    pub fn board(&self) -> &BoardId {
3896        &self.board
3897    }
3898
3899    /// Получить инструменты (`securities`) по текущему async owning-scope.
3900    pub async fn securities(&self, page_request: PageRequest) -> Result<Vec<Security>, MoexError> {
3901        self.client
3902            .securities_query(&self.engine, &self.market, &self.board, page_request)
3903            .await
3904    }
3905
3906    /// Создать асинхронный ленивый paginator `securities` по текущему async owning-scope.
3907    pub fn securities_pages(&self, page_limit: NonZeroU32) -> AsyncSecuritiesPages<'_> {
3908        self.client
3909            .securities_pages(&self.engine, &self.market, &self.board, page_limit)
3910    }
3911
3912    /// Получить снимки инструментов (`LOTSIZE` и `LAST`) для текущего owning-scope.
3913    pub async fn snapshots(&self) -> Result<Vec<SecuritySnapshot>, MoexError> {
3914        self.client
3915            .board_snapshots(&self.engine, &self.market, &self.board)
3916            .await
3917    }
3918
3919    /// Зафиксировать инструмент в рамках текущего `engine/market/board`.
3920    pub fn security<S>(self, security: S) -> Result<AsyncOwnedSecurityScope<'a>, ParseSecIdError>
3921    where
3922        S: TryInto<SecId>,
3923        S::Error: Into<ParseSecIdError>,
3924    {
3925        let security = security.try_into().map_err(Into::into)?;
3926        Ok(AsyncOwnedSecurityScope {
3927            client: self.client,
3928            engine: self.engine,
3929            market: self.market,
3930            board: self.board,
3931            security,
3932        })
3933    }
3934}
3935
3936#[cfg(feature = "async")]
3937impl<'a> AsyncOwnedSecurityResourceScope<'a> {
3938    /// Идентификатор инструмента текущего async owning-scope.
3939    pub fn secid(&self) -> &SecId {
3940        &self.security
3941    }
3942
3943    /// Получить карточку текущего инструмента.
3944    pub async fn info(&self) -> Result<Option<Security>, MoexError> {
3945        self.client.security_info(&self.security).await
3946    }
3947
3948    /// Получить режимы торгов (`boards`) для текущего инструмента.
3949    pub async fn boards(&self) -> Result<Vec<SecurityBoard>, MoexError> {
3950        self.client.security_boards(&self.security).await
3951    }
3952}
3953
3954#[cfg(feature = "async")]
3955impl<'a> AsyncOwnedSecurityScope<'a> {
3956    /// Идентификатор инструмента текущего async owning-scope.
3957    pub fn security(&self) -> &SecId {
3958        &self.security
3959    }
3960
3961    /// Получить стакан (`orderbook`) по текущему инструменту.
3962    pub async fn orderbook(&self) -> Result<Vec<OrderbookLevel>, MoexError> {
3963        self.client
3964            .orderbook(&self.engine, &self.market, &self.board, &self.security)
3965            .await
3966    }
3967
3968    #[cfg(feature = "history")]
3969    /// Получить диапазон доступных исторических дат по текущему инструменту.
3970    pub async fn history_dates(&self) -> Result<Option<HistoryDates>, MoexError> {
3971        self.client
3972            .history_dates(&self.engine, &self.market, &self.board, &self.security)
3973            .await
3974    }
3975
3976    #[cfg(feature = "history")]
3977    /// Получить исторические данные (`history`) по текущему инструменту.
3978    pub async fn history(
3979        &self,
3980        page_request: PageRequest,
3981    ) -> Result<Vec<HistoryRecord>, MoexError> {
3982        self.client
3983            .history_query(
3984                &self.engine,
3985                &self.market,
3986                &self.board,
3987                &self.security,
3988                page_request,
3989            )
3990            .await
3991    }
3992
3993    #[cfg(feature = "history")]
3994    /// Создать асинхронный ленивый paginator `history` по текущему инструменту.
3995    pub fn history_pages(&self, page_limit: NonZeroU32) -> AsyncHistoryPages<'_> {
3996        self.client.history_pages(
3997            &self.engine,
3998            &self.market,
3999            &self.board,
4000            &self.security,
4001            page_limit,
4002        )
4003    }
4004
4005    /// Получить сделки (`trades`) по текущему инструменту.
4006    pub async fn trades(&self, page_request: PageRequest) -> Result<Vec<Trade>, MoexError> {
4007        self.client
4008            .trades_query(
4009                &self.engine,
4010                &self.market,
4011                &self.board,
4012                &self.security,
4013                page_request,
4014            )
4015            .await
4016    }
4017
4018    /// Создать асинхронный ленивый paginator `trades` по текущему инструменту.
4019    pub fn trades_pages(&self, page_limit: NonZeroU32) -> AsyncTradesPages<'_> {
4020        self.client.trades_pages(
4021            &self.engine,
4022            &self.market,
4023            &self.board,
4024            &self.security,
4025            page_limit,
4026        )
4027    }
4028
4029    /// Получить свечи (`candles`) по текущему инструменту.
4030    pub async fn candles(
4031        &self,
4032        query: CandleQuery,
4033        page_request: PageRequest,
4034    ) -> Result<Vec<Candle>, MoexError> {
4035        self.client
4036            .candles_query(
4037                &self.engine,
4038                &self.market,
4039                &self.board,
4040                &self.security,
4041                query,
4042                page_request,
4043            )
4044            .await
4045    }
4046
4047    /// Создать асинхронный ленивый paginator `candles` по текущему инструменту.
4048    pub fn candles_pages(
4049        &self,
4050        query: CandleQuery,
4051        page_limit: NonZeroU32,
4052    ) -> AsyncCandlesPages<'_> {
4053        self.client.candles_pages(
4054            &self.engine,
4055            &self.market,
4056            &self.board,
4057            &self.security,
4058            query,
4059            page_limit,
4060        )
4061    }
4062}
4063
4064#[cfg(feature = "async")]
4065impl<'a> AsyncOwnedMarketSecurityScope<'a> {
4066    /// Имя торгового движка текущего async owning-scope.
4067    pub fn engine(&self) -> &EngineName {
4068        &self.engine
4069    }
4070
4071    /// Имя рынка текущего async owning-scope.
4072    pub fn market(&self) -> &MarketName {
4073        &self.market
4074    }
4075
4076    /// Идентификатор инструмента текущего async owning-scope.
4077    pub fn security(&self) -> &SecId {
4078        &self.security
4079    }
4080
4081    /// Получить карточку текущего инструмента на уровне рынка.
4082    pub async fn info(&self) -> Result<Option<Security>, MoexError> {
4083        self.client
4084            .market_security_info(&self.engine, &self.market, &self.security)
4085            .await
4086    }
4087
4088    /// Получить доступные границы свечей (`candleborders`) по текущему инструменту.
4089    pub async fn candle_borders(&self) -> Result<Vec<CandleBorder>, MoexError> {
4090        self.client
4091            .candle_borders(&self.engine, &self.market, &self.security)
4092            .await
4093    }
4094}
4095
4096#[cfg(feature = "blocking")]
4097impl<'a> IndexAnalyticsPages<'a> {
4098    /// Получить следующую страницу `index_analytics`.
4099    pub fn next_page(&mut self) -> Result<Option<Vec<IndexAnalytics>>, MoexError> {
4100        next_page_blocking(
4101            &mut self.pagination,
4102            |pagination| {
4103                self.client
4104                    .fetch_index_analytics_page(self.indexid, pagination)
4105            },
4106            |item| (item.trade_session_date(), item.secid().clone()),
4107        )
4108    }
4109
4110    /// Собрать все страницы `index_analytics` в один `Vec`.
4111    pub fn try_collect(mut self) -> Result<Vec<IndexAnalytics>, MoexError> {
4112        collect_pages_blocking(|| self.next_page())
4113    }
4114
4115    /// Алиас для [`Self::try_collect`].
4116    pub fn all(self) -> Result<Vec<IndexAnalytics>, MoexError> {
4117        self.try_collect()
4118    }
4119}
4120
4121#[cfg(feature = "blocking")]
4122impl<'a> SecuritiesPages<'a> {
4123    /// Получить следующую страницу `securities`.
4124    pub fn next_page(&mut self) -> Result<Option<Vec<Security>>, MoexError> {
4125        next_page_blocking(
4126            &mut self.pagination,
4127            |pagination| {
4128                self.client
4129                    .fetch_securities_page(self.engine, self.market, self.board, pagination)
4130            },
4131            |item| item.secid().clone(),
4132        )
4133    }
4134
4135    /// Собрать все страницы `securities` в один `Vec`.
4136    pub fn try_collect(mut self) -> Result<Vec<Security>, MoexError> {
4137        collect_pages_blocking(|| self.next_page())
4138    }
4139
4140    /// Алиас для [`Self::try_collect`].
4141    pub fn all(self) -> Result<Vec<Security>, MoexError> {
4142        self.try_collect()
4143    }
4144}
4145
4146#[cfg(feature = "blocking")]
4147impl<'a> GlobalSecuritiesPages<'a> {
4148    /// Получить следующую страницу глобального `securities`.
4149    pub fn next_page(&mut self) -> Result<Option<Vec<Security>>, MoexError> {
4150        next_page_blocking(
4151            &mut self.pagination,
4152            |pagination| self.client.fetch_global_securities_page(pagination),
4153            |item| item.secid().clone(),
4154        )
4155    }
4156
4157    /// Собрать все страницы глобального `securities` в один `Vec`.
4158    pub fn try_collect(mut self) -> Result<Vec<Security>, MoexError> {
4159        collect_pages_blocking(|| self.next_page())
4160    }
4161
4162    /// Алиас для [`Self::try_collect`].
4163    pub fn all(self) -> Result<Vec<Security>, MoexError> {
4164        self.try_collect()
4165    }
4166}
4167
4168#[cfg(all(feature = "blocking", feature = "news"))]
4169impl<'a> SiteNewsPages<'a> {
4170    /// Получить следующую страницу `sitenews`.
4171    pub fn next_page(&mut self) -> Result<Option<Vec<SiteNews>>, MoexError> {
4172        next_page_blocking(
4173            &mut self.pagination,
4174            |pagination| self.client.fetch_sitenews_page(pagination),
4175            SiteNews::id,
4176        )
4177    }
4178
4179    /// Собрать все страницы `sitenews` в один `Vec`.
4180    pub fn try_collect(mut self) -> Result<Vec<SiteNews>, MoexError> {
4181        collect_pages_blocking(|| self.next_page())
4182    }
4183
4184    /// Алиас для [`Self::try_collect`].
4185    pub fn all(self) -> Result<Vec<SiteNews>, MoexError> {
4186        self.try_collect()
4187    }
4188}
4189
4190#[cfg(all(feature = "blocking", feature = "news"))]
4191impl<'a> EventsPages<'a> {
4192    /// Получить следующую страницу `events`.
4193    pub fn next_page(&mut self) -> Result<Option<Vec<Event>>, MoexError> {
4194        next_page_blocking(
4195            &mut self.pagination,
4196            |pagination| self.client.fetch_events_page(pagination),
4197            Event::id,
4198        )
4199    }
4200
4201    /// Собрать все страницы `events` в один `Vec`.
4202    pub fn try_collect(mut self) -> Result<Vec<Event>, MoexError> {
4203        collect_pages_blocking(|| self.next_page())
4204    }
4205
4206    /// Алиас для [`Self::try_collect`].
4207    pub fn all(self) -> Result<Vec<Event>, MoexError> {
4208        self.try_collect()
4209    }
4210}
4211
4212#[cfg(feature = "blocking")]
4213impl<'a> MarketSecuritiesPages<'a> {
4214    /// Получить следующую страницу market-level `securities`.
4215    pub fn next_page(&mut self) -> Result<Option<Vec<Security>>, MoexError> {
4216        next_page_blocking(
4217            &mut self.pagination,
4218            |pagination| {
4219                self.client
4220                    .fetch_market_securities_page(self.engine, self.market, pagination)
4221            },
4222            |item| item.secid().clone(),
4223        )
4224    }
4225
4226    /// Собрать все страницы market-level `securities` в один `Vec`.
4227    pub fn try_collect(mut self) -> Result<Vec<Security>, MoexError> {
4228        collect_pages_blocking(|| self.next_page())
4229    }
4230
4231    /// Алиас для [`Self::try_collect`].
4232    pub fn all(self) -> Result<Vec<Security>, MoexError> {
4233        self.try_collect()
4234    }
4235}
4236
4237#[cfg(feature = "blocking")]
4238impl<'a> MarketTradesPages<'a> {
4239    /// Получить следующую страницу market-level `trades`.
4240    pub fn next_page(&mut self) -> Result<Option<Vec<Trade>>, MoexError> {
4241        next_page_blocking(
4242            &mut self.pagination,
4243            |pagination| {
4244                self.client
4245                    .fetch_market_trades_page(self.engine, self.market, pagination)
4246            },
4247            Trade::tradeno,
4248        )
4249    }
4250
4251    /// Собрать все страницы market-level `trades` в один `Vec`.
4252    pub fn try_collect(mut self) -> Result<Vec<Trade>, MoexError> {
4253        collect_pages_blocking(|| self.next_page())
4254    }
4255
4256    /// Алиас для [`Self::try_collect`].
4257    pub fn all(self) -> Result<Vec<Trade>, MoexError> {
4258        self.try_collect()
4259    }
4260}
4261
4262#[cfg(feature = "blocking")]
4263impl<'a> TradesPages<'a> {
4264    /// Получить следующую страницу `trades`.
4265    pub fn next_page(&mut self) -> Result<Option<Vec<Trade>>, MoexError> {
4266        next_page_blocking(
4267            &mut self.pagination,
4268            |pagination| {
4269                self.client.fetch_trades_page(
4270                    self.engine,
4271                    self.market,
4272                    self.board,
4273                    self.security,
4274                    pagination,
4275                )
4276            },
4277            Trade::tradeno,
4278        )
4279    }
4280
4281    /// Собрать все страницы `trades` в один `Vec`.
4282    pub fn try_collect(mut self) -> Result<Vec<Trade>, MoexError> {
4283        collect_pages_blocking(|| self.next_page())
4284    }
4285
4286    /// Алиас для [`Self::try_collect`].
4287    pub fn all(self) -> Result<Vec<Trade>, MoexError> {
4288        self.try_collect()
4289    }
4290}
4291
4292#[cfg(all(feature = "blocking", feature = "history"))]
4293impl<'a> HistoryPages<'a> {
4294    /// Получить следующую страницу `history`.
4295    pub fn next_page(&mut self) -> Result<Option<Vec<HistoryRecord>>, MoexError> {
4296        next_page_blocking(
4297            &mut self.pagination,
4298            |pagination| {
4299                self.client.fetch_history_page(
4300                    self.engine,
4301                    self.market,
4302                    self.board,
4303                    self.security,
4304                    pagination,
4305                )
4306            },
4307            HistoryRecord::tradedate,
4308        )
4309    }
4310
4311    /// Собрать все страницы `history` в один `Vec`.
4312    pub fn try_collect(mut self) -> Result<Vec<HistoryRecord>, MoexError> {
4313        collect_pages_blocking(|| self.next_page())
4314    }
4315
4316    /// Алиас для [`Self::try_collect`].
4317    pub fn all(self) -> Result<Vec<HistoryRecord>, MoexError> {
4318        self.try_collect()
4319    }
4320}
4321
4322#[cfg(feature = "blocking")]
4323impl<'a> SecStatsPages<'a> {
4324    /// Получить следующую страницу `secstats`.
4325    pub fn next_page(&mut self) -> Result<Option<Vec<SecStat>>, MoexError> {
4326        next_page_blocking(
4327            &mut self.pagination,
4328            |pagination| {
4329                self.client
4330                    .fetch_secstats_page(self.engine, self.market, pagination)
4331            },
4332            |item| (item.secid().clone(), item.boardid().clone()),
4333        )
4334    }
4335
4336    /// Собрать все страницы `secstats` в один `Vec`.
4337    pub fn try_collect(mut self) -> Result<Vec<SecStat>, MoexError> {
4338        collect_pages_blocking(|| self.next_page())
4339    }
4340
4341    /// Алиас для [`Self::try_collect`].
4342    pub fn all(self) -> Result<Vec<SecStat>, MoexError> {
4343        self.try_collect()
4344    }
4345}
4346
4347#[cfg(feature = "blocking")]
4348impl<'a> CandlesPages<'a> {
4349    /// Получить следующую страницу `candles`.
4350    pub fn next_page(&mut self) -> Result<Option<Vec<Candle>>, MoexError> {
4351        next_page_blocking(
4352            &mut self.pagination,
4353            |pagination| {
4354                self.client.fetch_candles_page(
4355                    self.engine,
4356                    self.market,
4357                    self.board,
4358                    self.security,
4359                    self.query,
4360                    pagination,
4361                )
4362            },
4363            Candle::begin,
4364        )
4365    }
4366
4367    /// Собрать все страницы `candles` в один `Vec`.
4368    pub fn try_collect(mut self) -> Result<Vec<Candle>, MoexError> {
4369        collect_pages_blocking(|| self.next_page())
4370    }
4371
4372    /// Алиас для [`Self::try_collect`].
4373    pub fn all(self) -> Result<Vec<Candle>, MoexError> {
4374        self.try_collect()
4375    }
4376}
4377
4378impl<K> PaginationTracker<K> {
4379    fn new(
4380        endpoint: impl Into<String>,
4381        page_limit: NonZeroU32,
4382        repeat_page_policy: RepeatPagePolicy,
4383    ) -> Self {
4384        Self {
4385            endpoint: endpoint.into().into_boxed_str(),
4386            page_limit,
4387            repeat_page_policy,
4388            start: 0,
4389            first_key_on_previous_page: None,
4390            finished: false,
4391        }
4392    }
4393
4394    fn next_page_request(&self) -> Option<Pagination> {
4395        if self.finished {
4396            return None;
4397        }
4398        Some(Pagination {
4399            start: Some(self.start),
4400            limit: Some(self.page_limit),
4401        })
4402    }
4403}
4404
4405impl<K> PaginationTracker<K>
4406where
4407    K: Eq,
4408{
4409    fn advance(
4410        &mut self,
4411        page_len: usize,
4412        first_key_on_page: Option<K>,
4413    ) -> Result<PaginationAdvance, MoexError> {
4414        let page_limit = self.page_limit.get();
4415
4416        if page_len == 0 {
4417            self.finished = true;
4418            return Ok(PaginationAdvance::EndOfPages);
4419        }
4420
4421        if let (Some(prev), Some(current)) = (&self.first_key_on_previous_page, &first_key_on_page)
4422            && prev == current
4423        {
4424            return match self.repeat_page_policy {
4425                RepeatPagePolicy::Error => Err(MoexError::PaginationStuck {
4426                    endpoint: self.endpoint.clone(),
4427                    start: self.start,
4428                    limit: page_limit,
4429                }),
4430            };
4431        }
4432
4433        self.first_key_on_previous_page = first_key_on_page;
4434
4435        if (page_len as u128) < u128::from(page_limit) {
4436            self.finished = true;
4437            return Ok(PaginationAdvance::YieldPage);
4438        }
4439
4440        self.start =
4441            self.start
4442                .checked_add(page_limit)
4443                .ok_or_else(|| MoexError::PaginationOverflow {
4444                    endpoint: self.endpoint.clone(),
4445                    start: self.start,
4446                    limit: page_limit,
4447                })?;
4448
4449        Ok(PaginationAdvance::YieldPage)
4450    }
4451}
4452
4453#[cfg(feature = "blocking")]
4454impl<'a> OwnedIndexScope<'a> {
4455    /// Идентификатор индекса текущего owning-scope.
4456    pub fn indexid(&self) -> &IndexId {
4457        &self.indexid
4458    }
4459
4460    /// Получить состав индекса (`analytics`) по текущему owning-scope.
4461    pub fn analytics(&self, page_request: PageRequest) -> Result<Vec<IndexAnalytics>, MoexError> {
4462        self.client
4463            .index_analytics_query(&self.indexid, page_request)
4464    }
4465
4466    /// Создать ленивый paginator страниц `analytics` для текущего индекса.
4467    pub fn analytics_pages(&self, page_limit: NonZeroU32) -> IndexAnalyticsPages<'_> {
4468        self.client.index_analytics_pages(&self.indexid, page_limit)
4469    }
4470}
4471
4472#[cfg(feature = "blocking")]
4473impl<'a> OwnedEngineScope<'a> {
4474    /// Имя торгового движка текущего owning-scope.
4475    pub fn engine(&self) -> &EngineName {
4476        &self.engine
4477    }
4478
4479    /// Получить доступные рынки (`markets`) для текущего движка.
4480    pub fn markets(&self) -> Result<Vec<Market>, MoexError> {
4481        self.client.markets(&self.engine)
4482    }
4483
4484    /// Получить обороты (`turnovers`) для текущего движка.
4485    pub fn turnovers(&self) -> Result<Vec<Turnover>, MoexError> {
4486        self.client.engine_turnovers(&self.engine)
4487    }
4488
4489    /// Зафиксировать рынок внутри текущего `engine`.
4490    pub fn market<M>(self, market: M) -> Result<OwnedMarketScope<'a>, ParseMarketNameError>
4491    where
4492        M: TryInto<MarketName>,
4493        M::Error: Into<ParseMarketNameError>,
4494    {
4495        let market = market.try_into().map_err(Into::into)?;
4496        Ok(OwnedMarketScope {
4497            client: self.client,
4498            engine: self.engine,
4499            market,
4500        })
4501    }
4502
4503    /// Shortcut для часто используемого рынка `shares`.
4504    pub fn shares(self) -> Result<OwnedMarketScope<'a>, ParseMarketNameError> {
4505        self.market("shares")
4506    }
4507}
4508
4509#[cfg(feature = "blocking")]
4510impl<'a> OwnedMarketScope<'a> {
4511    /// Имя торгового движка текущего owning-scope.
4512    pub fn engine(&self) -> &EngineName {
4513        &self.engine
4514    }
4515
4516    /// Имя рынка текущего owning-scope.
4517    pub fn market(&self) -> &MarketName {
4518        &self.market
4519    }
4520
4521    /// Получить режимы торгов (`boards`) для текущего рынка.
4522    pub fn boards(&self) -> Result<Vec<Board>, MoexError> {
4523        self.client.boards(&self.engine, &self.market)
4524    }
4525
4526    /// Получить инструменты (`securities`) на уровне текущего рынка.
4527    pub fn securities(&self, page_request: PageRequest) -> Result<Vec<Security>, MoexError> {
4528        self.client
4529            .market_securities_query(&self.engine, &self.market, page_request)
4530    }
4531
4532    /// Создать ленивый paginator страниц market-level `securities`.
4533    pub fn securities_pages(&self, page_limit: NonZeroU32) -> MarketSecuritiesPages<'_> {
4534        self.client
4535            .market_securities_pages(&self.engine, &self.market, page_limit)
4536    }
4537
4538    /// Получить market-level стакан (`orderbook`) для текущего рынка.
4539    pub fn orderbook(&self) -> Result<Vec<OrderbookLevel>, MoexError> {
4540        self.client.market_orderbook(&self.engine, &self.market)
4541    }
4542
4543    /// Получить market-level сделки (`trades`) для текущего рынка.
4544    pub fn trades(&self, page_request: PageRequest) -> Result<Vec<Trade>, MoexError> {
4545        self.client
4546            .market_trades_query(&self.engine, &self.market, page_request)
4547    }
4548
4549    /// Создать ленивый paginator страниц market-level `trades`.
4550    pub fn trades_pages(&self, page_limit: NonZeroU32) -> MarketTradesPages<'_> {
4551        self.client
4552            .market_trades_pages(&self.engine, &self.market, page_limit)
4553    }
4554
4555    /// Получить `secstats` для текущего рынка.
4556    pub fn secstats(&self, page_request: PageRequest) -> Result<Vec<SecStat>, MoexError> {
4557        self.client
4558            .secstats_query(&self.engine, &self.market, page_request)
4559    }
4560
4561    /// Создать ленивый paginator страниц `secstats`.
4562    pub fn secstats_pages(&self, page_limit: NonZeroU32) -> SecStatsPages<'_> {
4563        self.client
4564            .secstats_pages(&self.engine, &self.market, page_limit)
4565    }
4566
4567    /// Получить доступные границы свечей (`candleborders`) по инструменту.
4568    pub fn candle_borders(&self, security: &SecId) -> Result<Vec<CandleBorder>, MoexError> {
4569        self.client
4570            .candle_borders(&self.engine, &self.market, security)
4571    }
4572
4573    /// Зафиксировать инструмент в рамках текущего `engine/market`.
4574    pub fn security<S>(self, security: S) -> Result<OwnedMarketSecurityScope<'a>, ParseSecIdError>
4575    where
4576        S: TryInto<SecId>,
4577        S::Error: Into<ParseSecIdError>,
4578    {
4579        let security = security.try_into().map_err(Into::into)?;
4580        Ok(OwnedMarketSecurityScope {
4581            client: self.client,
4582            engine: self.engine,
4583            market: self.market,
4584            security,
4585        })
4586    }
4587
4588    /// Зафиксировать `board` внутри текущего `engine/market`.
4589    pub fn board<B>(self, board: B) -> Result<OwnedBoardScope<'a>, ParseBoardIdError>
4590    where
4591        B: TryInto<BoardId>,
4592        B::Error: Into<ParseBoardIdError>,
4593    {
4594        let board = board.try_into().map_err(Into::into)?;
4595        Ok(OwnedBoardScope {
4596            client: self.client,
4597            engine: self.engine,
4598            market: self.market,
4599            board,
4600        })
4601    }
4602}
4603
4604#[cfg(feature = "blocking")]
4605impl<'a> OwnedBoardScope<'a> {
4606    /// Имя торгового движка текущего owning-scope.
4607    pub fn engine(&self) -> &EngineName {
4608        &self.engine
4609    }
4610
4611    /// Имя рынка текущего owning-scope.
4612    pub fn market(&self) -> &MarketName {
4613        &self.market
4614    }
4615
4616    /// Идентификатор режима торгов текущего owning-scope.
4617    pub fn board(&self) -> &BoardId {
4618        &self.board
4619    }
4620
4621    /// Получить инструменты (`securities`) по текущему owning-scope.
4622    pub fn securities(&self, page_request: PageRequest) -> Result<Vec<Security>, MoexError> {
4623        self.client
4624            .securities_query(&self.engine, &self.market, &self.board, page_request)
4625    }
4626
4627    /// Создать ленивый paginator страниц `securities` по текущему owning-scope.
4628    pub fn securities_pages(&self, page_limit: NonZeroU32) -> SecuritiesPages<'_> {
4629        self.client
4630            .securities_pages(&self.engine, &self.market, &self.board, page_limit)
4631    }
4632
4633    /// Получить снимки инструментов (`LOTSIZE` и `LAST`) для текущего owning-scope.
4634    pub fn snapshots(&self) -> Result<Vec<SecuritySnapshot>, MoexError> {
4635        self.client
4636            .board_snapshots(&self.engine, &self.market, &self.board)
4637    }
4638
4639    /// Зафиксировать инструмент в рамках текущего `engine/market/board`.
4640    pub fn security<S>(self, security: S) -> Result<OwnedSecurityScope<'a>, ParseSecIdError>
4641    where
4642        S: TryInto<SecId>,
4643        S::Error: Into<ParseSecIdError>,
4644    {
4645        let security = security.try_into().map_err(Into::into)?;
4646        Ok(OwnedSecurityScope {
4647            client: self.client,
4648            engine: self.engine,
4649            market: self.market,
4650            board: self.board,
4651            security,
4652        })
4653    }
4654}
4655
4656#[cfg(feature = "blocking")]
4657impl<'a> OwnedSecurityResourceScope<'a> {
4658    /// Идентификатор инструмента текущего owning-scope.
4659    pub fn secid(&self) -> &SecId {
4660        &self.security
4661    }
4662
4663    /// Получить карточку текущего инструмента.
4664    pub fn info(&self) -> Result<Option<Security>, MoexError> {
4665        self.client.security_info(&self.security)
4666    }
4667
4668    /// Получить режимы торгов (`boards`) для текущего инструмента.
4669    pub fn boards(&self) -> Result<Vec<SecurityBoard>, MoexError> {
4670        self.client.security_boards(&self.security)
4671    }
4672}
4673
4674#[cfg(feature = "blocking")]
4675impl<'a> OwnedSecurityScope<'a> {
4676    /// Идентификатор инструмента текущего owning-scope.
4677    pub fn security(&self) -> &SecId {
4678        &self.security
4679    }
4680
4681    /// Получить стакан (`orderbook`) по текущему инструменту.
4682    pub fn orderbook(&self) -> Result<Vec<OrderbookLevel>, MoexError> {
4683        self.client
4684            .orderbook(&self.engine, &self.market, &self.board, &self.security)
4685    }
4686
4687    #[cfg(feature = "history")]
4688    /// Получить диапазон доступных исторических дат по текущему инструменту.
4689    pub fn history_dates(&self) -> Result<Option<HistoryDates>, MoexError> {
4690        self.client
4691            .history_dates(&self.engine, &self.market, &self.board, &self.security)
4692    }
4693
4694    #[cfg(feature = "history")]
4695    /// Получить исторические данные (`history`) по текущему инструменту.
4696    pub fn history(&self, page_request: PageRequest) -> Result<Vec<HistoryRecord>, MoexError> {
4697        self.client.history_query(
4698            &self.engine,
4699            &self.market,
4700            &self.board,
4701            &self.security,
4702            page_request,
4703        )
4704    }
4705
4706    #[cfg(feature = "history")]
4707    /// Создать ленивый paginator страниц `history` по текущему инструменту.
4708    pub fn history_pages(&self, page_limit: NonZeroU32) -> HistoryPages<'_> {
4709        self.client.history_pages(
4710            &self.engine,
4711            &self.market,
4712            &self.board,
4713            &self.security,
4714            page_limit,
4715        )
4716    }
4717
4718    /// Получить доступные границы свечей (`candleborders`) по текущему инструменту.
4719    pub fn candle_borders(&self) -> Result<Vec<CandleBorder>, MoexError> {
4720        self.client
4721            .candle_borders(&self.engine, &self.market, &self.security)
4722    }
4723
4724    /// Получить сделки (`trades`) по текущему инструменту.
4725    pub fn trades(&self, page_request: PageRequest) -> Result<Vec<Trade>, MoexError> {
4726        self.client.trades_query(
4727            &self.engine,
4728            &self.market,
4729            &self.board,
4730            &self.security,
4731            page_request,
4732        )
4733    }
4734
4735    /// Создать ленивый paginator страниц `trades` по текущему инструменту.
4736    pub fn trades_pages(&self, page_limit: NonZeroU32) -> TradesPages<'_> {
4737        self.client.trades_pages(
4738            &self.engine,
4739            &self.market,
4740            &self.board,
4741            &self.security,
4742            page_limit,
4743        )
4744    }
4745
4746    /// Получить свечи (`candles`) по текущему инструменту.
4747    pub fn candles(
4748        &self,
4749        query: CandleQuery,
4750        page_request: PageRequest,
4751    ) -> Result<Vec<Candle>, MoexError> {
4752        self.client.candles_query(
4753            &self.engine,
4754            &self.market,
4755            &self.board,
4756            &self.security,
4757            query,
4758            page_request,
4759        )
4760    }
4761
4762    /// Создать ленивый paginator `candles` по текущему инструменту.
4763    pub fn candles_pages(&self, query: CandleQuery, page_limit: NonZeroU32) -> CandlesPages<'_> {
4764        self.client.candles_pages(
4765            &self.engine,
4766            &self.market,
4767            &self.board,
4768            &self.security,
4769            query,
4770            page_limit,
4771        )
4772    }
4773}
4774
4775#[cfg(feature = "blocking")]
4776impl<'a> OwnedMarketSecurityScope<'a> {
4777    /// Имя торгового движка текущего owning-scope.
4778    pub fn engine(&self) -> &EngineName {
4779        &self.engine
4780    }
4781
4782    /// Имя рынка текущего owning-scope.
4783    pub fn market(&self) -> &MarketName {
4784        &self.market
4785    }
4786
4787    /// Идентификатор инструмента текущего owning-scope.
4788    pub fn security(&self) -> &SecId {
4789        &self.security
4790    }
4791
4792    /// Получить карточку текущего инструмента на уровне рынка.
4793    pub fn info(&self) -> Result<Option<Security>, MoexError> {
4794        self.client
4795            .market_security_info(&self.engine, &self.market, &self.security)
4796    }
4797
4798    /// Получить доступные границы свечей (`candleborders`) по текущему инструменту.
4799    pub fn candle_borders(&self) -> Result<Vec<CandleBorder>, MoexError> {
4800        self.client
4801            .candle_borders(&self.engine, &self.market, &self.security)
4802    }
4803}
4804
4805fn apply_iss_request_options(query: &mut Vec<(Box<str>, Box<str>)>, options: IssRequestOptions) {
4806    if let Some(metadata) = options.metadata_value() {
4807        query.push((ISS_META_PARAM.into(), metadata.as_query_value().into()));
4808    }
4809    if let Some(data) = options.data_value() {
4810        query.push((ISS_DATA_PARAM.into(), data.as_query_value().into()));
4811    }
4812    if let Some(version) = options.version_value() {
4813        query.push((ISS_VERSION_PARAM.into(), version.as_query_value().into()));
4814    }
4815    if let Some(json) = options.json_value() {
4816        query.push((ISS_JSON_PARAM.into(), json.into()));
4817    }
4818}
4819
4820/// Нормализовать raw endpoint-path к виду `relative/path.json`.
4821///
4822/// Запрещает query-string в пути и позволяет передавать как `iss/...`,
4823/// так и путь без префикса.
4824pub(super) fn normalize_raw_endpoint_path(path: Option<&str>) -> Result<Box<str>, MoexError> {
4825    let raw = path.ok_or(MoexError::MissingRawPath)?;
4826    let trimmed = raw.trim();
4827    if trimmed.is_empty() {
4828        return Err(MoexError::InvalidRawPath {
4829            path: raw.to_owned().into_boxed_str(),
4830            reason: "path must not be empty".into(),
4831        });
4832    }
4833    if trimmed.contains('?') {
4834        return Err(MoexError::InvalidRawPath {
4835            path: raw.to_owned().into_boxed_str(),
4836            reason: "query string is not allowed in path; use .param(...)".into(),
4837        });
4838    }
4839
4840    // Поддерживаем пути вида `/iss/...` и `iss/...`, чтобы API builder-а был гибким.
4841    let without_slash = trimmed.trim_start_matches('/');
4842    let endpoint = without_slash
4843        .strip_prefix("iss/")
4844        .unwrap_or(without_slash)
4845        .trim();
4846
4847    if endpoint.is_empty() {
4848        return Err(MoexError::InvalidRawPath {
4849            path: raw.to_owned().into_boxed_str(),
4850            reason: "endpoint path is empty after normalization".into(),
4851        });
4852    }
4853
4854    if endpoint.ends_with(".json") {
4855        return Ok(endpoint.to_owned().into_boxed_str());
4856    }
4857
4858    let mut normalized = endpoint.to_owned();
4859    normalized.push_str(".json");
4860    Ok(normalized.into_boxed_str())
4861}
4862
4863/// Преобразовать список `securities/{secid}` в опциональную единственную запись.
4864pub(super) fn optional_single_security(
4865    endpoint: &str,
4866    mut securities: Vec<Security>,
4867) -> Result<Option<Security>, MoexError> {
4868    if securities.len() > 1 {
4869        return Err(MoexError::UnexpectedSecurityRows {
4870            endpoint: endpoint.to_owned().into_boxed_str(),
4871            row_count: securities.len(),
4872        });
4873    }
4874    Ok(securities.pop())
4875}
4876
4877#[cfg(feature = "history")]
4878/// Преобразовать список `history/.../dates` в опциональную единственную запись.
4879pub(super) fn optional_single_history_dates(
4880    endpoint: &str,
4881    mut dates: Vec<HistoryDates>,
4882) -> Result<Option<HistoryDates>, MoexError> {
4883    if dates.len() > 1 {
4884        return Err(MoexError::UnexpectedHistoryDatesRows {
4885            endpoint: endpoint.to_owned().into_boxed_str(),
4886            row_count: dates.len(),
4887        });
4888    }
4889    Ok(dates.pop())
4890}
4891
4892/// Добавить параметры запроса свечей (`from`, `till`, `interval`) в URL.
4893pub(super) fn append_candle_query_to_url(endpoint_url: &mut Url, candle_query: CandleQuery) {
4894    let mut query_pairs = endpoint_url.query_pairs_mut();
4895    if let Some(from) = candle_query.from() {
4896        let from = from.format("%Y-%m-%d %H:%M:%S").to_string();
4897        query_pairs.append_pair(FROM_PARAM, &from);
4898    }
4899    if let Some(till) = candle_query.till() {
4900        let till = till.format("%Y-%m-%d %H:%M:%S").to_string();
4901        query_pairs.append_pair(TILL_PARAM, &till);
4902    }
4903    if let Some(interval) = candle_query.interval() {
4904        query_pairs.append_pair(INTERVAL_PARAM, interval.as_str());
4905    }
4906}
4907
4908/// Добавить параметры пагинации ISS (`start`, `limit`) в URL.
4909pub(super) fn append_pagination_to_url(endpoint_url: &mut Url, pagination: Pagination) {
4910    if pagination.start.is_none() && pagination.limit.is_none() {
4911        return;
4912    }
4913
4914    let mut query = endpoint_url.query_pairs_mut();
4915    if let Some(start) = pagination.start {
4916        let start = start.to_string();
4917        query.append_pair(START_PARAM, &start);
4918    }
4919    if let Some(limit) = pagination.limit {
4920        let limit = limit.get().to_string();
4921        query.append_pair(LIMIT_PARAM, &limit);
4922    }
4923}
4924
4925/// Быстрая эвристика, похож ли ответ на JSON.
4926pub(super) fn looks_like_json_payload(content_type: Option<&str>, payload: &str) -> bool {
4927    if content_type.is_some_and(contains_json_token_ascii_case_insensitive) {
4928        return true;
4929    }
4930
4931    let trimmed = payload.trim_start();
4932    trimmed.starts_with('{') || trimmed.starts_with('[')
4933}
4934
4935fn contains_json_token_ascii_case_insensitive(content_type: &str) -> bool {
4936    content_type
4937        .as_bytes()
4938        .windows(4)
4939        .any(|window| window.eq_ignore_ascii_case(b"json"))
4940}
4941
4942/// Взять безопасный префикс payload для диагностических сообщений.
4943pub(super) fn truncate_prefix(payload: &str, max_chars: usize) -> Box<str> {
4944    payload
4945        .chars()
4946        .take(max_chars)
4947        .collect::<String>()
4948        .into_boxed_str()
4949}