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}