Skip to main content

t_invest_sdk/
lib.rs

1use api::{
2    instruments_service_client::InstrumentsServiceClient,
3    market_data_service_client::MarketDataServiceClient,
4    market_data_stream_service_client::MarketDataStreamServiceClient,
5    operations_service_client::OperationsServiceClient,
6    operations_stream_service_client::OperationsStreamServiceClient,
7    orders_service_client::OrdersServiceClient,
8    orders_stream_service_client::OrdersStreamServiceClient,
9    sandbox_service_client::SandboxServiceClient, signal_service_client::SignalServiceClient,
10    stop_orders_service_client::StopOrdersServiceClient, users_service_client::UsersServiceClient,
11};
12use api::{MoneyValue, Quotation};
13use rust_decimal::prelude::ToPrimitive;
14use rust_decimal::Decimal;
15use thiserror::Error;
16use tonic::transport::ClientTlsConfig;
17use tonic::{
18    service::{interceptor::InterceptedService, Interceptor},
19    transport::Channel,
20};
21
22pub mod api;
23#[path = "google.api.rs"]
24pub mod google_api;
25
26/// Перехватчик для запросов T-Invest API.
27///
28/// Эта структура реализует трейт `Interceptor` из tonic для добавления
29/// необходимых заголовков к каждому API запросу, включая:
30/// - Аутентификацию с использованием предоставленного токена
31/// - ID отслеживания запроса
32/// - Имя приложения
33#[derive(Debug, Clone)]
34pub struct TInvestInterceptor {
35    pub token: String,
36}
37
38/// Ошибки, которые могут возникнуть при взаимодействии с T-Invest API.
39///
40/// Это перечисление представляет возможные типы ошибок, которые могут возникнуть:
41/// - `Transport`: Ошибки, связанные с сетевым подключением или транспортным уровнем
42/// - `Status`: Ошибки, возвращаемые самим API сервисом
43#[derive(Error, Debug)]
44pub enum TInvestError {
45    #[error(transparent)]
46    Transport(#[from] tonic::transport::Error),
47    #[error(transparent)]
48    Status(#[from] tonic::Status),
49}
50
51/// Представляет среду для подключения к T-Invest API.
52///
53/// Существует две возможные среды:
54/// - `Production`: Живая продакшн среда с реальными счетами и данными
55/// - `Sandbox`: Тестовая среда, которая симулирует продакшн API
56#[derive(Debug, Clone, Copy)]
57pub enum Environment {
58    Production,
59    Sandbox,
60}
61
62impl Environment {
63    /// Возвращает базовый URL для API на основе выбранной среды.
64    ///
65    /// # Возвращает
66    /// Статическую строку, содержащую полный базовый URL для API запросов.
67    fn api_url(&self) -> &'static str {
68        match self {
69            Environment::Production => "https://invest-public-api.tinkoff.ru:443/",
70            Environment::Sandbox => "https://sandbox-invest-public-api.tinkoff.ru:443/",
71        }
72    }
73}
74
75impl Interceptor for TInvestInterceptor {
76    /// Перехватывает каждый запрос для добавления необходимых заголовков перед отправкой в API.
77    ///
78    /// Эта реализация добавляет следующие заголовки к каждому запросу:
79    /// - `authorization`: Bearer токен для аутентификации
80    /// - `x-tracking-id`: Уникальный UUID для отслеживания запроса
81    /// - `x-app-name`: Идентификатор приложения
82    ///
83    /// # Аргументы
84    /// * `request` - Исходный запрос, который нужно изменить
85    ///
86    /// # Возвращает
87    /// Изменённый запрос с добавленными заголовками или статус ошибки
88    fn call(
89        &mut self,
90        mut request: tonic::Request<()>,
91    ) -> Result<tonic::Request<()>, tonic::Status> {
92        request.metadata_mut().append(
93            "authorization",
94            format!("bearer {}", self.token)
95                .parse()
96                .map_err(|_| tonic::Status::internal("Invalid authorization header"))?,
97        );
98
99        request.metadata_mut().append(
100            "x-tracking-id",
101            uuid::Uuid::new_v4()
102                .to_string()
103                .parse()
104                .map_err(|_| tonic::Status::internal("Invalid x-tracking-id"))?,
105        );
106
107        request.metadata_mut().append(
108            "x-app-name",
109            "artemevsevev.t-invest-sdk"
110                .parse()
111                .map_err(|_| tonic::Status::internal("Invalid x-app-name"))?,
112        );
113
114        Ok(request)
115    }
116}
117
118/// Основной SDK клиент для взаимодействия с T-Invest API.
119///
120/// Эта структура содержит канал и перехватчик
121///
122/// # Документация
123/// - [Описание](https://developer.tbank.ru/invest/intro/intro)
124/// - [Получить токен](https://developer.tbank.ru/invest/intro/intro/token#получить-токен)
125#[derive(Clone)]
126pub struct TInvestSdk {
127    channel: Channel,
128    interceptor: TInvestInterceptor,
129}
130
131impl TInvestSdk {
132    /// Создаёт новый экземпляр SDK, подключённый к продакшн среде.
133    ///
134    /// # Аргументы
135    /// * `token` - API токен для аутентификации
136    ///
137    /// # Возвращает
138    /// Result, содержащий либо инициализированный SDK, либо ошибку
139    pub async fn new_production(token: &str) -> Result<Self, TInvestError> {
140        Self::new(token, Environment::Production).await
141    }
142
143    /// Создаёт новый экземпляр SDK, подключённый к sandbox (тестовой) среде.
144    ///
145    /// # Аргументы
146    /// * `token` - API токен для аутентификации
147    ///
148    /// # Возвращает
149    /// Result, содержащий либо инициализированный SDK, либо ошибку
150    pub async fn new_sandbox(token: &str) -> Result<Self, TInvestError> {
151        Self::new(token, Environment::Sandbox).await
152    }
153
154    /// Создаёт новый экземпляр SDK с указанным токеном и средой.
155    ///
156    /// Это внутренний конструктор, используемый удобными методами
157    /// `new_production` и `new_sandbox`. Он устанавливает безопасный канал
158    /// к T-Invest API, используя TLS, и настраивает перехватчик аутентификации
159    /// с предоставленным токеном.
160    ///
161    /// # Аргументы
162    /// * `token` - API токен для аутентификации
163    /// * `environment` - Среда для подключения (Production или Sandbox)
164    ///
165    /// # Возвращает
166    /// Result, содержащий либо инициализированный SDK, либо TInvestError
167    ///
168    /// # Ошибки
169    /// Возвращает ошибку, если:
170    /// - Не удалось настроить TLS конфигурацию
171    /// - Невозможно установить соединение с каналом
172    pub async fn new(token: &str, environment: Environment) -> Result<Self, TInvestError> {
173        let tls = ClientTlsConfig::new().with_native_roots();
174        let channel = Channel::from_static(environment.api_url())
175            .tls_config(tls)?
176            .connect()
177            .await?;
178        let interceptor = TInvestInterceptor {
179            token: String::from(token),
180        };
181
182        Ok(Self {
183            channel,
184            interceptor,
185        })
186    }
187
188    /// Возвращает клиент для сервиса Instruments.
189    ///
190    /// Этот сервис предоставляет методы для работы с финансовыми инструментами,
191    /// включая акции, облигации, ETF, валюты и фьючерсы.
192    ///
193    /// # Документация:
194    ///   - [Описание сервиса](https://developer.tbank.ru/invest/services/instruments/head-instruments)
195    ///   - [gRPC-методы](https://developer.tbank.ru/invest/services/instruments/methods)
196    ///   - [Глоссарий и дополнительная информация о методах сервиса инструментов](https://developer.tbank.ru/invest/services/instruments/more-instrument)
197    ///   - [FAQ](https://developer.tbank.ru/invest/services/instruments/faq_instruments)
198    pub fn instruments(
199        &self,
200    ) -> InstrumentsServiceClient<InterceptedService<Channel, TInvestInterceptor>> {
201        InstrumentsServiceClient::with_interceptor(self.channel.clone(), self.interceptor.clone())
202    }
203
204    /// Возвращает клиент для сервиса Market Data.
205    ///
206    /// Этот сервис предоставляет методы для запроса рыночных данных, таких как
207    /// свечи, стаканы и последние цены.
208    ///
209    /// # Документация:
210    /// - [Описание сервиса](https://developer.tbank.ru/invest/services/quotes/head-marketdata)
211    /// - [gRPC-методы](https://developer.tbank.ru/invest/services/quotes/marketdata)
212    /// - [FAQ](https://developer.tbank.ru/invest/services/quotes/faq_marketdata)
213    pub fn market_data(
214        &self,
215    ) -> MarketDataServiceClient<InterceptedService<Channel, TInvestInterceptor>> {
216        MarketDataServiceClient::with_interceptor(self.channel.clone(), self.interceptor.clone())
217    }
218
219    /// Возвращает клиент для сервиса Market Data Stream.
220    ///
221    /// Этот сервис предоставляет потоковый доступ к рыночным данным в реальном времени,
222    /// включая свечи, стаканы и сделки.
223    ///
224    /// # Документация:
225    /// - [Описание сервиса](https://developer.tbank.ru/invest/services/quotes/head-marketdata)
226    /// - [gRPC-методы](https://developer.tbank.ru/invest/services/quotes/marketdata)
227    /// - [FAQ](https://developer.tbank.ru/invest/services/quotes/faq_marketdata)
228    pub fn market_data_stream(
229        &self,
230    ) -> MarketDataStreamServiceClient<InterceptedService<Channel, TInvestInterceptor>> {
231        MarketDataStreamServiceClient::with_interceptor(
232            self.channel.clone(),
233            self.interceptor.clone(),
234        )
235    }
236
237    /// Возвращает клиент для сервиса Operations.
238    ///
239    /// Этот сервис предоставляет методы для работы с операциями по счёту,
240    /// такими как получение истории операций и деталей операций.
241    ///
242    /// # Документация:
243    /// - [Описание сервиса](https://developer.tbank.ru/invest/services/operations/head-operations)
244    /// - [Особенности методов сервиса операций](https://developer.tbank.ru/invest/services/operations/operations_problems)
245    /// - [gRPC-методы](https://developer.tbank.ru/invest/services/operations/methods)
246    /// - [FAQ](https://developer.tbank.ru/invest/services/operations/faq_operations)
247    pub fn operations(
248        &self,
249    ) -> OperationsServiceClient<InterceptedService<Channel, TInvestInterceptor>> {
250        OperationsServiceClient::with_interceptor(self.channel.clone(), self.interceptor.clone())
251    }
252
253    /// Возвращает клиент для сервиса Operations Stream.
254    ///
255    /// Этот сервис предоставляет потоковый доступ к операциям по счёту в реальном времени.
256    ///
257    /// # Документация:
258    /// - [Описание сервиса](https://developer.tbank.ru/invest/services/operations/head-operations)
259    /// - [Особенности методов сервиса операций](https://developer.tbank.ru/invest/services/operations/operations_problems)
260    /// - [gRPC-методы](https://developer.tbank.ru/invest/services/operations/methods)
261    /// - [FAQ](https://developer.tbank.ru/invest/services/operations/faq_operations)
262    pub fn operations_stream(
263        &self,
264    ) -> OperationsStreamServiceClient<InterceptedService<Channel, TInvestInterceptor>> {
265        OperationsStreamServiceClient::with_interceptor(
266            self.channel.clone(),
267            self.interceptor.clone(),
268        )
269    }
270
271    /// Возвращает клиент для сервиса Orders.
272    ///
273    /// Этот сервис предоставляет методы для размещения, отмены и получения информации
274    /// о заявках.
275    ///
276    /// # Документация:
277    /// - [Описание сервиса](https://developer.tbank.ru/invest/services/orders/head-orders)
278    /// - [gRPC-методы](https://developer.tbank.ru/invest/services/orders/methods)
279    /// - [Асинхронный метод выставления заявок](https://developer.tbank.ru/invest/services/orders/async)
280    /// - [FAQ](https://developer.tbank.ru/invest/services/orders/faq_orders)
281    pub fn orders(&self) -> OrdersServiceClient<InterceptedService<Channel, TInvestInterceptor>> {
282        OrdersServiceClient::with_interceptor(self.channel.clone(), self.interceptor.clone())
283    }
284
285    /// Возвращает клиент для сервиса Orders Stream.
286    ///
287    /// Этот сервис предоставляет потоковый доступ к обновлениям статуса заявок в реальном времени.
288    ///
289    /// # Документация:
290    /// - [Описание сервиса](https://developer.tbank.ru/invest/services/orders/head-orders)
291    /// - [gRPC-методы](https://developer.tbank.ru/invest/services/orders/methods)
292    /// - [Стрим заявок](https://developer.tbank.ru/invest/services/orders/orders_state_stream)
293    /// - [FAQ](https://developer.tbank.ru/invest/services/orders/faq_orders)
294    pub fn orders_stream(
295        &self,
296    ) -> OrdersStreamServiceClient<InterceptedService<Channel, TInvestInterceptor>> {
297        OrdersStreamServiceClient::with_interceptor(self.channel.clone(), self.interceptor.clone())
298    }
299
300    /// Возвращает клиент для сервиса Sandbox.
301    ///
302    /// Этот сервис предоставляет методы для работы с sandbox (тестовой) средой,
303    /// включая создание и удаление sandbox счетов.
304    ///
305    /// # Документация:
306    /// - [Описание сервиса](https://developer.tbank.ru/invest/intro/developer/sandbox/)
307    /// - [gRPC-методы](https://developer.tbank.ru/invest/intro/developer/sandbox/methods)
308    /// - [Песочница и prod](https://developer.tbank.ru/invest/intro/developer/sandbox/url_difference)
309    /// - [FAQ](https://developer.tbank.ru/invest/intro/developer/sandbox/faq_sandbox)
310    pub fn sandbox(&self) -> SandboxServiceClient<InterceptedService<Channel, TInvestInterceptor>> {
311        SandboxServiceClient::with_interceptor(self.channel.clone(), self.interceptor.clone())
312    }
313
314    /// Возвращает клиент для сервиса Signal.
315    ///
316    /// Этот сервис предоставляет методы для работы с инвестиционными сигналами и рекомендациями.
317    ///
318    /// # Документация:
319    /// - [Описание сервиса](https://developer.tbank.ru/invest/services/signals/head-signals)
320    /// - [gRPC-методы](https://developer.tbank.ru/invest/services/signals/methods)
321    pub fn signal(&self) -> SignalServiceClient<InterceptedService<Channel, TInvestInterceptor>> {
322        SignalServiceClient::with_interceptor(self.channel.clone(), self.interceptor.clone())
323    }
324
325    /// Возвращает клиент для сервиса Stop Orders.
326    ///
327    /// Этот сервис предоставляет методы для размещения, отмены и получения информации
328    /// о стоп-заявках.
329    ///
330    /// # Документация:
331    /// - [Описание сервиса](https://developer.tbank.ru/invest/services/stop-orders/head-stoporders)
332    /// - [gRPC-методы](https://developer.tbank.ru/invest/services/stop-orders/stoporders)
333    /// - [FAQ](https://developer.tbank.ru/invest/services/stop-orders/faq_stoporders)
334    pub fn stop_orders(
335        &self,
336    ) -> StopOrdersServiceClient<InterceptedService<Channel, TInvestInterceptor>> {
337        StopOrdersServiceClient::with_interceptor(self.channel.clone(), self.interceptor.clone())
338    }
339
340    /// Возвращает клиент для сервиса Users.
341    ///
342    /// Этот сервис предоставляет методы для получения информации о пользовательских счетах
343    /// и их деталях.
344    ///
345    /// # Документация:
346    /// - [Описание сервиса](https://developer.tbank.ru/invest/services/accounts/head-account)
347    /// - [gRPC-методы](https://developer.tbank.ru/invest/services/accounts/users)
348    /// - [FAQ](https://developer.tbank.ru/invest/services/accounts/faq_users)
349    pub fn users(&self) -> UsersServiceClient<InterceptedService<Channel, TInvestInterceptor>> {
350        UsersServiceClient::with_interceptor(self.channel.clone(), self.interceptor.clone())
351    }
352}
353
354/// Преобразует Quotation в Decimal.
355///
356/// Тип Quotation представляет число как целую часть (units) и дробную часть (nano).
357/// Эта реализация объединяет их в единое значение Decimal.
358impl From<Quotation> for Decimal {
359    fn from(quotation: Quotation) -> Self {
360        Decimal::new(quotation.units, 0) + Decimal::new(quotation.nano as i64, 9).normalize()
361    }
362}
363
364/// Преобразует MoneyValue в Decimal.
365///
366/// Тип MoneyValue представляет денежную сумму как целую часть (units) и дробную часть (nano).
367/// Эта реализация объединяет их в единое значение Decimal, игнорируя поле валюты.
368impl From<MoneyValue> for Decimal {
369    fn from(money_value: MoneyValue) -> Self {
370        Decimal::new(money_value.units, 0) + Decimal::new(money_value.nano as i64, 9).normalize()
371    }
372}
373
374/// Пытается преобразовать Decimal в Quotation.
375///
376/// Эта реализация разделяет значение Decimal на целые единицы и нано-части
377/// для создания Quotation. Возвращает ошибку, если преобразование невозможно.
378impl TryFrom<Decimal> for Quotation {
379    type Error = String;
380
381    fn try_from(value: Decimal) -> Result<Self, Self::Error> {
382        let units = value
383            .trunc()
384            .to_i64()
385            .ok_or_else(|| format!("Can't convert decimal {} to quotation", value))?;
386        let nano = (value.fract() * Decimal::new(1_000_000_000, 0))
387            .to_i32()
388            .ok_or_else(|| format!("Can't convert decimal {} to quotation", value))?;
389
390        Ok(Quotation { units, nano })
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use rust_decimal_macros::dec;
397
398    use super::*;
399
400    #[test]
401    fn quotation_to_decimal() {
402        assert_eq!(dec!(0), Quotation { units: 0, nano: 0 }.into());
403
404        assert_eq!(
405            dec!(100),
406            Quotation {
407                units: 100,
408                nano: 0
409            }
410            .into()
411        );
412
413        assert_eq!(
414            dec!(-100),
415            Quotation {
416                units: -100,
417                nano: 0
418            }
419            .into()
420        );
421
422        assert_eq!(
423            dec!(114.25),
424            Quotation {
425                units: 114,
426                nano: 250000000
427            }
428            .into()
429        );
430
431        assert_eq!(
432            dec!(-200.20),
433            Quotation {
434                units: -200,
435                nano: -200000000
436            }
437            .into()
438        );
439
440        assert_eq!(
441            dec!(-0.01),
442            Quotation {
443                units: -0,
444                nano: -10000000
445            }
446            .into()
447        );
448
449        assert_eq!(
450            dec!(999.999999999),
451            Quotation {
452                units: 999,
453                nano: 999999999
454            }
455            .into()
456        );
457
458        assert_eq!(
459            dec!(-999.999999999),
460            Quotation {
461                units: -999,
462                nano: -999999999
463            }
464            .into()
465        );
466    }
467
468    #[test]
469    fn money_value_to_decimal() {
470        assert_eq!(
471            dec!(0),
472            MoneyValue {
473                units: 0,
474                nano: 0,
475                currency: "".to_string()
476            }
477            .into()
478        );
479
480        assert_eq!(
481            dec!(100),
482            MoneyValue {
483                units: 100,
484                nano: 0,
485                currency: "".to_string()
486            }
487            .into()
488        );
489
490        assert_eq!(
491            dec!(-100),
492            MoneyValue {
493                units: -100,
494                nano: 0,
495                currency: "".to_string()
496            }
497            .into()
498        );
499
500        assert_eq!(
501            dec!(114.25),
502            MoneyValue {
503                units: 114,
504                nano: 250000000,
505                currency: "".to_string()
506            }
507            .into()
508        );
509
510        assert_eq!(
511            dec!(-200.20),
512            MoneyValue {
513                units: -200,
514                nano: -200000000,
515                currency: "".to_string()
516            }
517            .into()
518        );
519
520        assert_eq!(
521            dec!(-0.01),
522            MoneyValue {
523                units: -0,
524                nano: -10000000,
525                currency: "".to_string()
526            }
527            .into()
528        );
529
530        assert_eq!(
531            dec!(999.999999999),
532            MoneyValue {
533                units: 999,
534                nano: 999999999,
535                currency: "".to_string()
536            }
537            .into()
538        );
539
540        assert_eq!(
541            dec!(-999.999999999),
542            MoneyValue {
543                units: -999,
544                nano: -999999999,
545                currency: "".to_string()
546            }
547            .into()
548        );
549    }
550
551    #[test]
552    fn decimal_to_quotation() {
553        assert_eq!(Ok(Quotation { units: 0, nano: 0 }), dec!(0).try_into());
554
555        assert_eq!(
556            Ok(Quotation {
557                units: 114,
558                nano: 250000000,
559            }),
560            dec!(114.25).try_into()
561        );
562
563        assert_eq!(
564            Ok(Quotation {
565                units: -200,
566                nano: -200000000,
567            }),
568            dec!(-200.20).try_into()
569        );
570
571        assert_eq!(
572            Ok(Quotation {
573                units: -0,
574                nano: -10000000,
575            }),
576            dec!(-0.01).try_into()
577        );
578
579        assert_eq!(
580            Ok(Quotation {
581                units: 999,
582                nano: 999999999,
583            }),
584            dec!(999.999999999).try_into()
585        );
586
587        assert_eq!(
588            Ok(Quotation {
589                units: -999,
590                nano: -999999999,
591            }),
592            dec!(-999.999999999).try_into()
593        );
594    }
595}