upstox_rust_sdk/client.rs
1//! ## Get started
2//!
3//! The code below represents an API client that connects websockets and uses handlers to handle incoming data.
4//!
5//!
6//! ```rust
7//! use {
8//! dotenvy::dotenv,
9//! futures::future::join_all,
10//! std::{collections::HashSet, env},
11//! tokio::signal,
12//! upstox_rust_sdk::{
13//! client::{ApiClient, AutomateLoginConfig, LoginConfig, MailProvider, WSConnectConfig},
14//! constants::UPLINK_API_KEY_ENV,
15//! models::ws::{
16//! market_data_feed_v3_message::{MessageDataV3, ModeTypeV3},
17//! portfolio_feed_request::PortfolioUpdateType,
18//! portfolio_feed_response::PortfolioFeedResponse,
19//! },
20//! protos::market_data_feed_v3::FeedResponse as MarketDataFeedV3Response,
21//! ws_client::MarketDataV3Call,
22//! },
23//! };
24
25//! #[tokio::main]
26//! async fn main() {
27//! tracing_subscriber::fmt::init();
28//! let _ = dotenv();
29
30//! let portfolio_feed_handler = |data: PortfolioFeedResponse| {
31//! println!("{:?}", data);
32//! };
33//! let market_data_feed_v3_handler = |data: MarketDataFeedV3Response| {
34//! println!("{:?}", data);
35//! };
36
37//! let api_key: String = env::var(UPLINK_API_KEY_ENV).unwrap();
38//! let (fetch_instruments, schedule_refresh_instruments) = (false, false);
39
40//! // ApiClient with websockets connected and handler specified
41//! let (api_client, tasks_vec) = ApiClient::new(
42//! &api_key,
43//! LoginConfig {
44//! authorize: true,
45//! automate_login_config: Some(AutomateLoginConfig {
46//! automate_login: true,
47//! schedule_login: false,
48//! automate_fetching_otp: true,
49//! mail_provider: Some(MailProvider::Google),
50//! }),
51//! },
52//! fetch_instruments,
53//! schedule_refresh_instruments,
54//! // Configuration to connect and handle websocket data.
55//! WSConnectConfig {
56//! connect_portfolio_stream: true,
57//! connect_market_data_stream_v3: true,
58//! // Select which portfolio data to fetch
59//! portfolio_stream_update_types: Some(HashSet::from([
60//! PortfolioUpdateType::Order,
61//! PortfolioUpdateType::Position,
62//! PortfolioUpdateType::Holding,
63//! ])),
64//! // Handle portfolio data feed
65//! portfolio_feed_callback: Some(Box::new(portfolio_feed_handler)),
66//! // Handle market data feed
67//! market_data_feed_v3_callback: Some(Box::new(market_data_feed_v3_handler)),
68//! },
69//! )
70//! .await
71//! .unwrap();
72
73//! let api_client = api_client.lock().await;
74//! api_client
75//! .send_market_data_feed_v3_message(MarketDataV3Call::SubscribeInstrument(MessageDataV3 {
76//! mode: ModeTypeV3::Full,
77//! instrument_keys: vec![
78//! "NSE_INDEX|NIFTY LARGEMID250".to_string(),
79//! "NSE_INDEX|Nifty Auto".to_string(),
80//! "NSE_INDEX|Nifty Midcap 50".to_string(),
81//! ],
82//! }))
83//! .await
84//! .unwrap();
85
86//! // This ensures that app continues running until the websockets die if connected or until SIGINT occurs
87//! tokio::select! {
88//! _ = join_all(tasks_vec) => {}
89//! _ = signal::ctrl_c() => {}
90//! };
91//! }
92
93//!
94//! ```
95//!
96//! The code below represents an API client that logs user in, allowing usage of authorised endpoints, automates the login process including fetching OTP via GMail and scheduling it daily.
97//!
98//!
99//! ```rust
100//!
101//! #[tokio::main]
102//! async fn main() {
103//! tracing_subscriber::fmt::init();
104//! let _ = dotenv();
105
106//! let api_key: String = env::var(UPLINK_API_KEY_ENV).unwrap();
107//! let (fetch_instruments, schedule_refresh_instruments) = (false, false);
108
109//! // ApiClient which logs in automatically and schedules relogin daily when token expires
110//! let (_api_client, tasks_vec) = ApiClient::new(
111//! &api_key,
112//! LoginConfig {
113//! authorize: true,
114//! automate_login_config: Some(AutomateLoginConfig {
115//! // geckodriver or chromedriver binary must be running locally with port specified in env to use automatic login or schedule login.
116//! // ./geckodriver --binary "~/.local/share/flatpak/exports/bin/org.mozilla.firefox" --profile-root "~/.var/app/org.mozilla.firefox/cache/mozilla/firefox/cv70hco5.default-release"
117
118//! // Either GOOGLE_AUTHORIZATION_CODE environment must not be set or must be recent if using for the first time or refresh_token.txt has been deleted
119//! automate_login: true,
120//! // Relogin is scheduled at 3:30 AM IST daily.
121//! schedule_login: true,
122//! // Fetch OTP automatically from inbox by providing email IMAP access using env variables. For GMail, login page will be opened in your default web browsers and permission must be granted.
123//! automate_fetching_otp: true,
124//! mail_provider: Some(MailProvider::Google),
125//! }),
126//! },
127//! fetch_instruments,
128//! schedule_refresh_instruments,
129//! WSConnectConfig {
130//! connect_portfolio_stream: false,
131//! connect_market_data_stream_v3: false,
132//! portfolio_stream_update_types: None,
133//! portfolio_feed_callback: None::<Box<dyn FnMut(PortfolioFeedResponse) + Send + Sync>>,
134//! market_data_feed_v3_callback: None::<
135//! Box<dyn FnMut(MarketDataFeedV3Response) + Send + Sync>,
136//! >,
137//! },
138//! )
139//! .await
140//! .unwrap();
141
142//! // This ensures that app continues running until SIGINT occurs
143//! tokio::select! {
144//! _ = join_all(tasks_vec) => {}
145//! _ = signal::ctrl_c() => {}
146//! };
147//! }
148//!
149//!
150//! ```
151//!
152//! The code below represents an API client that fetches the complete list of instruments and stores it in the API Client. It also schedules daily refresh of the list.
153//!
154//! ```rust
155//! #[tokio::main]
156//! async fn main() {
157//! tracing_subscriber::fmt::init();
158//! let _ = dotenv();
159
160//! let api_key: String = env::var(UPLINK_API_KEY_ENV).unwrap();
161//! let (fetch_instruments, schedule_refresh_instruments) = (true, true);
162
163//! // ApiClient which fetches instruments, schedules instruments refresh daily and stores it in ApiClient
164//! let (api_client, tasks_vec) = ApiClient::new(
165//! &api_key,
166//! LoginConfig {
167//! authorize: false,
168//! automate_login_config: Some(AutomateLoginConfig {
169//! automate_login: false,
170//! schedule_login: false,
171//! automate_fetching_otp: false,
172//! mail_provider: Some(MailProvider::Google),
173//! }),
174//! },
175//! // Fetch all instruments data from UPSTOX and store it in the ApiClient.
176//! fetch_instruments,
177//! // Refresh instruments data daily at 6:30 AM.
178//! schedule_refresh_instruments,
179//! WSConnectConfig {
180//! connect_portfolio_stream: false,
181//! connect_market_data_stream_v3: false,
182//! portfolio_stream_update_types: None,
183//! portfolio_feed_callback: None::<Box<dyn FnMut(PortfolioFeedResponse) + Send + Sync>>,
184//! market_data_feed_v3_callback: None::<
185//! Box<dyn FnMut(MarketDataFeedV3Response) + Send + Sync>,
186//! >,
187//! },
188//! )
189//! .await
190//! .unwrap();
191
192//! {
193//! // Ensure that api_client mutex guard goes out of scope when no longer needed.
194//! let api_client: MutexGuard<ApiClient> = api_client.lock().await;
195//! print!(
196//! "{:?}",
197//! api_client
198//! .instruments
199//! .as_ref()
200//! .unwrap()
201//! .get(&ExchangeSegment::NseIndex)
202//! .unwrap()
203//! .get("INDEX")
204//! .unwrap()
205//! );
206//! std::io::stdout().flush().unwrap();
207//! }
208
209//! // This ensures that app continues running until SIGINT occurs
210//! tokio::select! {
211//! _ = join_all(tasks_vec) => {}
212//! _ = signal::ctrl_c() => {}
213//! };
214//! }
215//! ```
216//!
217//! The code below represents the basic usage of API Client to fetch data via REST APIs.
218//!
219//! ```rust
220//! use {
221//! dotenvy::dotenv,
222//! futures::future::join_all,
223//! std::env,
224//! tokio::{
225//! signal,
226//! sync::MutexGuard,
227//! time::{Instant, sleep},
228//! },
229//! tracing::info,
230//! upstox_rust_sdk::{
231//! client::{ApiClient, AutomateLoginConfig, LoginConfig, MailProvider, WSConnectConfig},
232//! constants::UPLINK_API_KEY_ENV,
233//! models::{
234//! ProductType, TransactionType,
235//! charges::brokerage_details_request::BrokerageDetailsRequest,
236//! success_response::SuccessResponse,
237//! user::{
238//! fund_and_margin_request::SegmentType,
239//! fund_and_margin_response::FundAndMarginResponse, profile_response::ProfileResponse,
240//! },
241//! ws::portfolio_feed_response::PortfolioFeedResponse,
242//! },
243//! protos::market_data_feed_v3::FeedResponse as MarketDataFeedV3Response,
244//! rate_limiter::RateLimitExceeded,
245//! },
246//! };
247
248//! #[tokio::main]
249//! async fn main() {
250//! tracing_subscriber::fmt::init();
251//! let _ = dotenv();
252
253//! let api_key: String = env::var(UPLINK_API_KEY_ENV).unwrap();
254//! let (fetch_instruments, schedule_refresh_instruments) = (true, false);
255
256//! let (api_client, tasks_vec) = ApiClient::new(
257//! &api_key,
258//! LoginConfig {
259//! authorize: true,
260//! automate_login_config: Some(AutomateLoginConfig {
261//! automate_login: true,
262//! schedule_login: false,
263//! automate_fetching_otp: true,
264//! mail_provider: Some(MailProvider::Google),
265//! }),
266//! },
267//! fetch_instruments,
268//! schedule_refresh_instruments,
269//! WSConnectConfig {
270//! connect_portfolio_stream: false,
271//! connect_market_data_stream_v3: false,
272//! portfolio_stream_update_types: None,
273//! portfolio_feed_callback: None::<Box<dyn FnMut(PortfolioFeedResponse) + Send + Sync>>,
274//! market_data_feed_v3_callback: None::<
275//! Box<dyn FnMut(MarketDataFeedV3Response) + Send + Sync>,
276//! >,
277//! },
278//! )
279//! .await
280//! .unwrap();
281
282//! {
283//! let api_client: MutexGuard<ApiClient> = api_client.lock().await;
284
285//! // User Endpoints
286//! let _profile: SuccessResponse<ProfileResponse> =
287//! api_client.get_profile().await.unwrap().unwrap();
288//! info!("Profile: {:?}", _profile);
289
290//! // Charges Endpoints
291//! let _charges_result = api_client
292//! .get_brokerage_details(BrokerageDetailsRequest {
293//! instrument_token: "NSE_EQ|INE806T01012".to_string(),
294//! quantity: 2,
295//! product: ProductType::I,
296//! transaction_type: TransactionType::Buy,
297//! price: 1575.00,
298//! })
299//! .await;
300
301//! if let Err(e) = _charges_result {
302//! let next_allowed_at = match e {
303//! RateLimitExceeded::PerSecond { next_allowed_at } => next_allowed_at,
304//! RateLimitExceeded::PerMinute { next_allowed_at } => next_allowed_at,
305//! RateLimitExceeded::PerThirtyMinutes { next_allowed_at } => next_allowed_at,
306//! };
307//! info!(
308//! "Rate limit exceeded. Sleeping for {:?}",
309//! next_allowed_at - Instant::now()
310//! );
311//! sleep(next_allowed_at - Instant::now()).await;
312//! }
313
314//! let charges = api_client
315//! .get_brokerage_details(BrokerageDetailsRequest {
316//! instrument_token: "NSE_EQ|INE806T01012".to_string(),
317//! quantity: 2,
318//! product: ProductType::I,
319//! transaction_type: TransactionType::Buy,
320//! price: 1575.00,
321//! })
322//! .await
323//! .unwrap()
324//! .unwrap();
325
326//! info!("Charges: {:?}", charges);
327
328//! let _funds_and_margin: SuccessResponse<FundAndMarginResponse> = api_client
329//! .get_fund_and_margin(Some(SegmentType::Sec))
330//! .await
331//! .unwrap()
332//! .unwrap();
333
334//! // This is just for usage illustration. All endpoints in https://upstox.com/developer/api-documentation/open-api are available via the ApiClient.
335//! }
336
337//! // This ensures that app continues running until SIGINT occurs
338//! tokio::select! {
339//! _ = join_all(tasks_vec) => {}
340//! _ = signal::ctrl_c() => {}
341//! };
342//! }
343//!
344//! ```
345
346use {
347 crate::{
348 constants::{
349 APIVersion, BaseUrlType, RATE_LIMIT_PER_MINUTE, RATE_LIMIT_PER_SECOND,
350 RATE_LIMIT_PER_THIRTY_MINUTES,
351 },
352 models::{
353 ExchangeSegment,
354 error_response::ErrorResponse,
355 instruments::instruments_response::InstrumentsResponse,
356 success_response::SuccessResponse,
357 user::profile_response::ProfileResponse,
358 ws::{
359 portfolio_feed_request::PortfolioUpdateType,
360 portfolio_feed_response::PortfolioFeedResponse,
361 },
362 },
363 protos::market_data_feed_v3::FeedResponse as MarketDataFeedV3Response,
364 rate_limiter::{ApiRateLimiter, RateLimitExceeded},
365 utils::create_url,
366 ws_client::{MarketDataFeedV3Client, PortfolioFeedClient},
367 },
368 chrono::FixedOffset,
369 ezsockets::Client as EzClient,
370 reqwest::{Client as ReqwestClient, Method, RequestBuilder, Response},
371 serde::Serialize,
372 std::{
373 collections::{HashMap, HashSet},
374 sync::Arc,
375 },
376 tokio::{
377 sync::{Mutex, MutexGuard},
378 task::JoinHandle,
379 },
380 tokio_cron_scheduler::{Job, JobScheduler},
381 tracing::info,
382};
383
384pub struct ApiClient {
385 pub(crate) client: ReqwestClient,
386 pub(crate) api_key: String,
387 pub(crate) token: Option<String>,
388 pub instruments: Option<HashMap<ExchangeSegment, HashMap<String, Vec<InstrumentsResponse>>>>,
389 pub portfolio_feed_client:
390 Option<EzClient<PortfolioFeedClient<Box<dyn FnMut(PortfolioFeedResponse) + Send + Sync>>>>,
391 pub market_data_feed_v3_client: Option<
392 EzClient<MarketDataFeedV3Client<Box<dyn FnMut(MarketDataFeedV3Response) + Send + Sync>>>,
393 >,
394 pub rate_limiter: ApiRateLimiter,
395}
396
397impl ApiClient {
398 pub async fn new(
399 api_key: &str,
400 login_config: LoginConfig,
401 fetch_instruments: bool,
402 schedule_refresh_instruments: bool,
403 ws_connect_config: WSConnectConfig,
404 ) -> Result<(Arc<Mutex<ApiClient>>, Vec<JoinHandle<()>>), String> {
405 let api_client = ApiClient {
406 client: ReqwestClient::new(),
407 api_key: api_key.to_string(),
408 token: None,
409 instruments: None,
410 portfolio_feed_client: None,
411 market_data_feed_v3_client: None,
412 rate_limiter: ApiRateLimiter::new(
413 RATE_LIMIT_PER_SECOND,
414 RATE_LIMIT_PER_MINUTE,
415 RATE_LIMIT_PER_THIRTY_MINUTES,
416 ),
417 };
418
419 let shared_api_client = Arc::new(Mutex::new(api_client));
420 let mut tasks_vec = Vec::<JoinHandle<()>>::new();
421
422 let scheduler = JobScheduler::new().await.unwrap();
423 scheduler.start().await.unwrap();
424 scheduler.shutdown_on_ctrl_c();
425
426 if fetch_instruments {
427 let mut api_client = shared_api_client.lock().await;
428 api_client.instruments =
429 Some(Self::parse_instruments(api_client.get_instruments().await?));
430 if schedule_refresh_instruments {
431 Self::schedule_refresh_instruments(&scheduler, &shared_api_client).await;
432 }
433 }
434
435 if !login_config.authorize {
436 return Ok((shared_api_client, tasks_vec));
437 }
438
439 {
440 let mut api_client = shared_api_client.lock().await;
441 api_client.login(&login_config).await?;
442
443 if ws_connect_config.connect_portfolio_stream {
444 let portfolio_feed_task = api_client
445 .connect_portfolio_feed(
446 ws_connect_config.portfolio_stream_update_types,
447 ws_connect_config.portfolio_feed_callback,
448 )
449 .await?;
450 tasks_vec.push(portfolio_feed_task);
451 }
452 if ws_connect_config.connect_market_data_stream_v3 {
453 let market_data_feed_v3_task = api_client
454 .connect_market_data_feed_v3(ws_connect_config.market_data_feed_v3_callback)
455 .await?;
456 tasks_vec.push(market_data_feed_v3_task);
457 }
458 }
459
460 if let Some(automate_login_config) = login_config.automate_login_config {
461 if automate_login_config.schedule_login {
462 Self::schedule_auto_login(&scheduler, &shared_api_client, login_config).await;
463 }
464 }
465 Ok((shared_api_client, tasks_vec))
466 }
467
468 pub(crate) async fn get(
469 &self,
470 endpoint: &str,
471 authorized: bool,
472 params: Option<&Vec<(String, String)>>,
473 base_url_type: BaseUrlType,
474 api_version: APIVersion,
475 ) -> Result<Response, RateLimitExceeded> {
476 self.request::<()>(
477 Method::GET,
478 endpoint,
479 authorized,
480 params,
481 None,
482 None,
483 base_url_type,
484 api_version,
485 )
486 .await
487 }
488
489 pub(crate) async fn post<T>(
490 &self,
491 endpoint: &str,
492 authorized: bool,
493 json_body: Option<&T>,
494 form_body: Option<&Vec<(String, String)>>,
495 base_url_type: BaseUrlType,
496 api_version: APIVersion,
497 ) -> Result<Response, RateLimitExceeded>
498 where
499 T: Serialize + ?Sized,
500 {
501 self.request(
502 Method::POST,
503 endpoint,
504 authorized,
505 None,
506 json_body,
507 form_body,
508 base_url_type,
509 api_version,
510 )
511 .await
512 }
513
514 pub(crate) async fn put<T>(
515 &self,
516 endpoint: &str,
517 authorized: bool,
518 json_body: Option<&T>,
519 form_body: Option<&Vec<(String, String)>>,
520 base_url_type: BaseUrlType,
521 api_version: APIVersion,
522 ) -> Result<Response, RateLimitExceeded>
523 where
524 T: Serialize + ?Sized,
525 {
526 self.request(
527 Method::PUT,
528 endpoint,
529 authorized,
530 None,
531 json_body,
532 form_body,
533 base_url_type,
534 api_version,
535 )
536 .await
537 }
538
539 pub(crate) async fn delete<T>(
540 &self,
541 endpoint: &str,
542 authorized: bool,
543 params: Option<&Vec<(String, String)>>,
544 json_body: Option<&T>,
545 base_url_type: BaseUrlType,
546 api_version: APIVersion,
547 ) -> Result<Response, RateLimitExceeded>
548 where
549 T: Serialize + ?Sized,
550 {
551 self.request::<T>(
552 Method::DELETE,
553 endpoint,
554 authorized,
555 params,
556 json_body,
557 None,
558 base_url_type,
559 api_version,
560 )
561 .await
562 }
563
564 async fn request<T>(
565 &self,
566 method: Method,
567 endpoint: &str,
568 authorized: bool,
569 params: Option<&Vec<(String, String)>>,
570 json_body: Option<&T>,
571 form_body: Option<&Vec<(String, String)>>,
572 base_url_type: BaseUrlType,
573 api_version: APIVersion,
574 ) -> Result<Response, RateLimitExceeded>
575 where
576 T: Serialize + ?Sized,
577 {
578 let rate_limit_check_result: Option<RateLimitExceeded> =
579 self.rate_limiter.check_rate_limit(endpoint).await;
580 if rate_limit_check_result.is_some() {
581 return Err(rate_limit_check_result.unwrap());
582 }
583 let url: String = create_url(base_url_type, api_version, endpoint);
584
585 if authorized && !self.token.is_some() {
586 panic!(
587 "{}",
588 format!(
589 "Cannot make authorized requests as client is not authorized: {}",
590 url
591 )
592 );
593 }
594
595 let mut request: RequestBuilder = match method {
596 Method::GET => self.client.get(url),
597 Method::POST => self.client.post(url),
598 Method::PUT => self.client.put(url),
599 Method::DELETE => self.client.delete(url),
600 _ => panic!("Unsupported HTTP Method"),
601 };
602
603 if let Some(req_params) = params {
604 request = request.query(req_params);
605 }
606
607 if let Some(req_json_body) = json_body {
608 request = request.json(req_json_body);
609 }
610
611 if let Some(req_form_body) = form_body {
612 request = request.form(req_form_body);
613 }
614
615 if authorized {
616 request = request.bearer_auth(&self.token.as_ref().unwrap());
617 }
618 request = request.header("Accept", "application/json");
619 Ok(request.send().await.unwrap())
620 }
621
622 pub(crate) async fn verify_authorization(&mut self) -> bool {
623 let verify_response: Result<SuccessResponse<ProfileResponse>, ErrorResponse> =
624 self.get_profile().await.unwrap();
625 verify_response.map_or_else(
626 |_| {
627 info!("Upstox saved access token invalid");
628 false
629 },
630 |_| {
631 info!("Using valid access token from file");
632 true
633 },
634 )
635 }
636
637 async fn schedule_refresh_instruments(
638 scheduler: &JobScheduler,
639 shared_api_client: &Arc<Mutex<ApiClient>>,
640 ) {
641 let shared_api_client_clone: Arc<Mutex<ApiClient>> = Arc::clone(&shared_api_client);
642 let job: Job = Job::new_async_tz(
643 "0 30 06 * * *",
644 FixedOffset::east_opt(19800).unwrap(),
645 move |_, _| {
646 let api_client: Arc<Mutex<ApiClient>> = Arc::clone(&shared_api_client_clone);
647 Box::pin(async move {
648 let mut client: MutexGuard<ApiClient> = api_client.lock().await;
649 if let Ok(instruments) = client.get_instruments().await {
650 client.instruments = Some(Self::parse_instruments(instruments));
651 }
652 })
653 },
654 )
655 .unwrap();
656
657 scheduler.add(job).await.unwrap();
658 }
659
660 async fn schedule_auto_login(
661 scheduler: &JobScheduler,
662 shared_api_client: &Arc<Mutex<ApiClient>>,
663 login_config: LoginConfig,
664 ) {
665 let shared_api_client_clone: Arc<Mutex<ApiClient>> = Arc::clone(&shared_api_client);
666 let job: Job = Job::new_async_tz(
667 "0 30 03 * * *",
668 FixedOffset::east_opt(19800).unwrap(),
669 move |_, _| {
670 let api_client: Arc<Mutex<ApiClient>> = Arc::clone(&shared_api_client_clone);
671 let login_config: LoginConfig = login_config.clone();
672 Box::pin(async move {
673 let mut client: MutexGuard<ApiClient> = api_client.lock().await;
674 client.login(&login_config).await.unwrap();
675 })
676 },
677 )
678 .unwrap();
679
680 scheduler.add(job).await.unwrap();
681 }
682}
683
684#[derive(Clone)]
685pub struct LoginConfig {
686 pub authorize: bool,
687 pub automate_login_config: Option<AutomateLoginConfig>,
688}
689
690#[derive(Clone, Copy)]
691pub struct AutomateLoginConfig {
692 pub automate_login: bool,
693 pub schedule_login: bool, // At 3:30 AM IST daily
694 pub automate_fetching_otp: bool,
695 pub mail_provider: Option<MailProvider>,
696}
697
698#[derive(Clone, Copy)]
699pub enum MailProvider {
700 Google,
701}
702
703pub struct WSConnectConfig {
704 pub connect_portfolio_stream: bool,
705 pub connect_market_data_stream_v3: bool,
706 pub portfolio_stream_update_types: Option<HashSet<PortfolioUpdateType>>,
707 pub portfolio_feed_callback: Option<Box<dyn FnMut(PortfolioFeedResponse) + Send + Sync>>,
708 pub market_data_feed_v3_callback:
709 Option<Box<dyn FnMut(MarketDataFeedV3Response) + Send + Sync>>,
710}