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