Skip to main content

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}