1use crate::application::auth::WebsocketInfo;
7use crate::application::config::Config;
8use crate::application::http::HttpClient;
9use crate::application::interfaces::account::AccountService;
10use crate::application::interfaces::costs::CostsService;
11use crate::application::interfaces::market::MarketService;
12use crate::application::interfaces::operations::OperationsService;
13use crate::application::interfaces::order::OrderService;
14use crate::application::interfaces::sentiment::SentimentService;
15use crate::application::interfaces::watchlist::WatchlistService;
16use crate::error::AppError;
17use crate::model::requests::RecentPricesRequest;
18use crate::model::requests::{
19 AddToWatchlistRequest, CloseCostsRequest, CreateWatchlistRequest, EditCostsRequest,
20 OpenCostsRequest, UpdateWorkingOrderRequest,
21};
22use crate::model::requests::{
23 ClosePositionRequest, CreateOrderRequest, CreateWorkingOrderRequest, UpdatePositionRequest,
24};
25use crate::model::responses::{
26 AccountActivityResponse, AccountsResponse, OrderConfirmationResponse, PositionsResponse,
27 TransactionHistoryResponse, WorkingOrdersResponse,
28};
29use crate::model::responses::{
30 AccountPreferencesResponse, ApplicationDetailsResponse, CategoriesResponse,
31 CategoryInstrumentsResponse, ClientSentimentResponse, CostsHistoryResponse,
32 CreateWatchlistResponse, DBEntryResponse, DurableMediumResponse, HistoricalPricesResponse,
33 IndicativeCostsResponse, MarketNavigationResponse, MarketSearchResponse, MarketSentiment,
34 MultipleMarketDetailsResponse, SinglePositionResponse, StatusResponse,
35 WatchlistMarketsResponse, WatchlistsResponse,
36};
37use crate::model::responses::{
38 ClosePositionResponse, CreateOrderResponse, CreateWorkingOrderResponse, UpdatePositionResponse,
39};
40use crate::model::retry::backoff_delay;
41use crate::model::streaming::{
42 StreamingAccountDataField, StreamingChartField, StreamingMarketField, StreamingPriceField,
43 get_streaming_account_data_fields, get_streaming_chart_fields, get_streaming_market_fields,
44 get_streaming_price_fields,
45};
46use crate::presentation::account::AccountFields;
47use crate::presentation::chart::{ChartData, ChartScale};
48use crate::presentation::market::{MarketData, MarketDetails};
49use crate::presentation::price::PriceData;
50use crate::presentation::trade::TradeFields;
51use async_trait::async_trait;
52use futures::StreamExt;
53use lightstreamer_rs::client::{LightstreamerClient, LogType, Transport};
54use lightstreamer_rs::subscription::{
55 ChannelSubscriptionListener, Snapshot, Subscription, SubscriptionMode,
56};
57use lightstreamer_rs::utils::{LightstreamerError, setup_signal_hook};
58use reqwest::StatusCode;
59use std::collections::HashSet;
60use std::sync::Arc;
61use std::time::Duration;
62use tokio::sync::{Mutex, Notify, mpsc};
63use tokio::task::JoinHandle;
64use tokio::time::sleep;
65use tracing::{debug, error, info, warn};
66
67const MAX_CONNECTION_ATTEMPTS: u64 = 3;
68
69const MARKET_DETAILS_CONCURRENCY: usize = 6;
75
76const GRACEFUL_CLOSE_MARKER: &str = "No more requests to fulfill";
83
84#[must_use]
96#[inline]
97fn is_graceful_close(error: &LightstreamerError) -> bool {
98 match error {
99 LightstreamerError::Connection(msg)
100 | LightstreamerError::Protocol(msg)
101 | LightstreamerError::InvalidState(msg) => msg.contains(GRACEFUL_CLOSE_MARKER),
102 _ => false,
103 }
104}
105
106#[must_use]
122fn spawn_shutdown_fanout(source: Arc<Notify>, targets: Vec<Arc<Notify>>) -> JoinHandle<()> {
123 tokio::spawn(async move {
124 source.notified().await;
125 for target in &targets {
126 target.notify_one();
127 }
128 })
129}
130
131async fn abort_and_drain_tasks(tasks: &mut Vec<JoinHandle<()>>) {
138 for handle in tasks.drain(..) {
139 handle.abort();
140 let _ = handle.await;
142 }
143}
144
145#[must_use]
161fn is_transient_confirmation_error(err: &AppError) -> bool {
162 match err {
163 AppError::RateLimitExceeded | AppError::Network(_) | AppError::NotFound => true,
164 AppError::Unexpected(status) => {
165 *status == StatusCode::NOT_FOUND || status.is_server_error()
166 }
167 _ => false,
168 }
169}
170
171pub struct Client {
176 http_client: Arc<HttpClient>,
177}
178
179impl Client {
180 pub fn try_new() -> Result<Self, AppError> {
198 let http_client = Arc::new(HttpClient::new_lazy(Config::default())?);
199 Ok(Self { http_client })
200 }
201
202 pub fn with_config(config: Config) -> Result<Self, AppError> {
264 let http_client = Arc::new(HttpClient::new_lazy(config)?);
265 Ok(Self { http_client })
266 }
267
268 #[inline]
276 #[must_use]
277 pub fn config(&self) -> &Config {
278 self.http_client.config()
279 }
280
281 pub async fn ws_info(&self) -> Result<WebsocketInfo, AppError> {
295 self.http_client.ws_info().await
296 }
297
298 #[deprecated(
303 note = "use ws_info() which reuses the cached session and returns a typed error instead of a default-on-error WebsocketInfo"
304 )]
305 pub async fn get_ws_info(&self) -> WebsocketInfo {
306 self.ws_info().await.unwrap_or_default()
307 }
308}
309
310#[async_trait]
311impl MarketService for Client {
312 async fn search_markets(&self, search_term: &str) -> Result<MarketSearchResponse, AppError> {
313 let path = format!("markets?searchTerm={}", search_term);
314 info!("Searching markets with term: {}", search_term);
315 let result: MarketSearchResponse = self.http_client.get(&path, Some(1)).await?;
316 debug!("{} markets found", result.markets.len());
317 Ok(result)
318 }
319
320 async fn get_market_details(&self, epic: &str) -> Result<MarketDetails, AppError> {
321 let path = format!("markets/{epic}");
322 info!("Getting market details: {}", epic);
323 let market_details: MarketDetails = self.http_client.get(&path, Some(3)).await?;
327 debug!("Market details obtained for: {}", epic);
328 Ok(market_details)
329 }
330
331 async fn get_multiple_market_details(
332 &self,
333 epics: &[String],
334 ) -> Result<MultipleMarketDetailsResponse, AppError> {
335 if epics.is_empty() {
336 return Ok(MultipleMarketDetailsResponse::default());
337 } else if epics.len() > 50 {
338 return Err(AppError::InvalidInput(
339 "The maximum number of EPICs is 50".to_string(),
340 ));
341 }
342
343 let epics_str = epics.join(",");
344 let path = format!("markets?epics={}", epics_str);
345 debug!(
346 "Getting market details for {} EPICs in a batch",
347 epics.len()
348 );
349
350 let response: MultipleMarketDetailsResponse = self.http_client.get(&path, Some(2)).await?;
351
352 Ok(response)
353 }
354
355 async fn get_historical_prices(
356 &self,
357 epic: &str,
358 resolution: &str,
359 from: &str,
360 to: &str,
361 ) -> Result<HistoricalPricesResponse, AppError> {
362 let path = format!(
363 "prices/{}?resolution={}&from={}&to={}",
364 epic, resolution, from, to
365 );
366 info!("Getting historical prices for: {}", epic);
367 let result: HistoricalPricesResponse = self.http_client.get(&path, Some(3)).await?;
368 debug!("Historical prices obtained for: {}", epic);
369 Ok(result)
370 }
371
372 async fn get_historical_prices_by_date_range(
373 &self,
374 epic: &str,
375 resolution: &str,
376 start_date: &str,
377 end_date: &str,
378 ) -> Result<HistoricalPricesResponse, AppError> {
379 let path = format!("prices/{}/{}/{}/{}", epic, resolution, start_date, end_date);
380 info!(
381 "Getting historical prices for epic: {}, resolution: {}, from: {} to: {}",
382 epic, resolution, start_date, end_date
383 );
384 let result: HistoricalPricesResponse = self.http_client.get(&path, Some(2)).await?;
385 debug!(
386 "Historical prices obtained for epic: {}, {} data points",
387 epic,
388 result.prices.len()
389 );
390 Ok(result)
391 }
392
393 async fn get_recent_prices(
394 &self,
395 params: &RecentPricesRequest<'_>,
396 ) -> Result<HistoricalPricesResponse, AppError> {
397 let mut query_params = Vec::new();
398
399 if let Some(res) = params.resolution {
400 query_params.push(format!("resolution={}", res));
401 }
402 if let Some(f) = params.from {
403 query_params.push(format!("from={}", f));
404 }
405 if let Some(t) = params.to {
406 query_params.push(format!("to={}", t));
407 }
408 if let Some(max) = params.max_points {
409 query_params.push(format!("max={}", max));
410 }
411 if let Some(size) = params.page_size {
412 query_params.push(format!("pageSize={}", size));
413 }
414 if let Some(num) = params.page_number {
415 query_params.push(format!("pageNumber={}", num));
416 }
417
418 let query_string = if query_params.is_empty() {
419 String::new()
420 } else {
421 format!("?{}", query_params.join("&"))
422 };
423
424 let path = format!("prices/{}{}", params.epic, query_string);
425 info!("Getting recent prices for epic: {}", params.epic);
426 let result: HistoricalPricesResponse = self.http_client.get(&path, Some(3)).await?;
427 debug!(
428 "Recent prices obtained for epic: {}, {} data points",
429 params.epic,
430 result.prices.len()
431 );
432 Ok(result)
433 }
434
435 async fn get_historical_prices_by_count_v1(
436 &self,
437 epic: &str,
438 resolution: &str,
439 num_points: u32,
440 ) -> Result<HistoricalPricesResponse, AppError> {
441 let path = format!("prices/{}/{}/{}", epic, resolution, num_points);
442 info!(
443 "Getting historical prices (v1) for epic: {}, resolution: {}, points: {}",
444 epic, resolution, num_points
445 );
446 let result: HistoricalPricesResponse = self.http_client.get(&path, Some(1)).await?;
447 debug!(
448 "Historical prices (v1) obtained for epic: {}, {} data points",
449 epic,
450 result.prices.len()
451 );
452 Ok(result)
453 }
454
455 async fn get_historical_prices_by_count_v2(
456 &self,
457 epic: &str,
458 resolution: &str,
459 num_points: u32,
460 ) -> Result<HistoricalPricesResponse, AppError> {
461 let path = format!("prices/{}/{}/{}", epic, resolution, num_points);
462 info!(
463 "Getting historical prices (v2) for epic: {}, resolution: {}, points: {}",
464 epic, resolution, num_points
465 );
466 let result: HistoricalPricesResponse = self.http_client.get(&path, Some(2)).await?;
467 debug!(
468 "Historical prices (v2) obtained for epic: {}, {} data points",
469 epic,
470 result.prices.len()
471 );
472 Ok(result)
473 }
474
475 async fn get_market_navigation(&self) -> Result<MarketNavigationResponse, AppError> {
476 let path = "marketnavigation";
477 info!("Getting top-level market navigation nodes");
478 let result: MarketNavigationResponse = self.http_client.get(path, Some(1)).await?;
479 debug!("{} navigation nodes found", result.nodes.len());
480 debug!("{} markets found at root level", result.markets.len());
481 Ok(result)
482 }
483
484 async fn get_market_navigation_node(
485 &self,
486 node_id: &str,
487 ) -> Result<MarketNavigationResponse, AppError> {
488 let path = format!("marketnavigation/{}", node_id);
489 info!("Getting market navigation node: {}", node_id);
490 let result: MarketNavigationResponse = self.http_client.get(&path, Some(1)).await?;
491 debug!("{} child nodes found", result.nodes.len());
492 debug!("{} markets found in node {}", result.markets.len(), node_id);
493 Ok(result)
494 }
495
496 async fn get_all_markets(&self) -> Result<Vec<MarketData>, AppError> {
497 let max_depth = 6;
498 info!(
499 "Starting comprehensive market hierarchy traversal (max {} levels)",
500 max_depth
501 );
502
503 let root_response = self.get_market_navigation().await?;
504 info!(
505 "Root navigation: {} nodes, {} markets at top level",
506 root_response.nodes.len(),
507 root_response.markets.len()
508 );
509
510 let mut seen_epics: HashSet<String> = HashSet::new();
514 let mut all_markets: Vec<MarketData> = Vec::new();
515 for market in root_response.markets {
516 if seen_epics.insert(market.epic.clone()) {
517 all_markets.push(market);
518 }
519 }
520 let mut nodes_to_process = root_response.nodes;
521 let mut processed_levels = 0;
522
523 while !nodes_to_process.is_empty() && processed_levels < max_depth {
524 let mut next_level_nodes = Vec::new();
525 let mut level_market_count = 0;
526
527 info!(
528 "Processing level {} with {} nodes",
529 processed_levels,
530 nodes_to_process.len()
531 );
532
533 for node in &nodes_to_process {
534 match self.get_market_navigation_node(&node.id).await {
535 Ok(node_response) => {
536 let node_markets = node_response.markets.len();
537 let node_children = node_response.nodes.len();
538
539 if node_markets > 0 || node_children > 0 {
540 debug!(
541 "Node '{}' (level {}): {} markets, {} child nodes",
542 node.name, processed_levels, node_markets, node_children
543 );
544 }
545
546 for market in node_response.markets {
549 if seen_epics.insert(market.epic.clone()) {
550 all_markets.push(market);
551 level_market_count += 1;
552 }
553 }
554 next_level_nodes.extend(node_response.nodes);
555 }
556 Err(e) => {
557 tracing::error!(
558 "Failed to get markets for node '{}' at level {}: {:?}",
559 node.name,
560 processed_levels,
561 e
562 );
563 }
564 }
565 }
566
567 info!(
568 "Level {} completed: {} markets found, {} nodes for next level",
569 processed_levels,
570 level_market_count,
571 next_level_nodes.len()
572 );
573
574 nodes_to_process = next_level_nodes;
575 processed_levels += 1;
576 }
577
578 info!(
579 "Market hierarchy traversal completed: {} total markets found across {} levels",
580 all_markets.len(),
581 processed_levels
582 );
583
584 Ok(all_markets)
585 }
586
587 async fn get_vec_db_entries(&self) -> Result<Vec<DBEntryResponse>, AppError> {
588 info!("Getting all markets from hierarchy for DB entries");
589
590 let all_markets = self.get_all_markets().await?;
591 info!("Collected {} markets from hierarchy", all_markets.len());
592
593 let mut vec_db_entries: Vec<DBEntryResponse> = all_markets
594 .iter()
595 .map(DBEntryResponse::from)
596 .filter(|entry| !entry.epic.is_empty())
597 .collect();
598
599 info!("Created {} DB entries from markets", vec_db_entries.len());
600
601 let mut symbol_info: std::collections::HashMap<String, (String, String)> =
607 std::collections::HashMap::new();
608 for entry in &vec_db_entries {
609 if entry.symbol.is_empty() || entry.epic.is_empty() {
610 continue;
611 }
612 symbol_info
613 .entry(entry.symbol.clone())
614 .or_insert_with(|| (entry.epic.clone(), entry.expiry.clone()));
615 }
616
617 info!(
618 "Found {} unique symbols to fetch expiry dates for",
619 symbol_info.len()
620 );
621
622 let symbol_expiry_map: std::collections::HashMap<String, String> =
626 futures::stream::iter(symbol_info)
627 .map(|(symbol, (epic, fallback_expiry))| async move {
628 match self.get_market_details(&epic).await {
629 Ok(market_details) => {
630 let expiry_date = market_details
631 .instrument
632 .expiry_details
633 .as_ref()
634 .map(|details| details.last_dealing_date.clone())
635 .unwrap_or_else(|| market_details.instrument.expiry.clone());
636
637 info!(
638 symbol = %symbol,
639 expiry = %expiry_date,
640 "fetched expiry date for symbol"
641 );
642 (symbol, expiry_date)
643 }
644 Err(e) => {
645 tracing::error!(
646 "Failed to get market details for epic {} (symbol {}): {:?}",
647 epic,
648 symbol,
649 e
650 );
651 (symbol, fallback_expiry)
652 }
653 }
654 })
655 .buffer_unordered(MARKET_DETAILS_CONCURRENCY)
656 .collect()
657 .await;
658
659 for entry in &mut vec_db_entries {
660 if let Some(expiry_date) = symbol_expiry_map.get(&entry.symbol) {
661 entry.expiry = expiry_date.clone();
662 }
663 }
664
665 info!("Updated expiry dates for {} entries", vec_db_entries.len());
666 Ok(vec_db_entries)
667 }
668
669 async fn get_categories(&self) -> Result<CategoriesResponse, AppError> {
670 info!("Getting all categories of instruments");
671 let result: CategoriesResponse = self.http_client.get("categories", Some(1)).await?;
672 debug!("{} categories found", result.categories.len());
673 Ok(result)
674 }
675
676 async fn get_category_instruments(
677 &self,
678 category_id: &str,
679 page_number: Option<u32>,
680 page_size: Option<u32>,
681 ) -> Result<CategoryInstrumentsResponse, AppError> {
682 let mut path = format!("categories/{}/instruments", category_id);
683
684 let mut query_params = Vec::new();
685 if let Some(page) = page_number {
686 query_params.push(format!("pageNumber={}", page));
687 }
688 if let Some(size) = page_size {
689 if size > 1000 {
690 return Err(AppError::InvalidInput(
691 "pageSize cannot exceed 1000".to_string(),
692 ));
693 }
694 query_params.push(format!("pageSize={}", size));
695 }
696
697 if !query_params.is_empty() {
698 path = format!("{}?{}", path, query_params.join("&"));
699 }
700
701 info!(
702 "Getting instruments for category: {} (page: {:?}, size: {:?})",
703 category_id, page_number, page_size
704 );
705 let result: CategoryInstrumentsResponse = self.http_client.get(&path, Some(1)).await?;
706 debug!(
707 "{} instruments found in category {}",
708 result.instruments.len(),
709 category_id
710 );
711 Ok(result)
712 }
713}
714
715#[async_trait]
716impl AccountService for Client {
717 async fn get_accounts(&self) -> Result<AccountsResponse, AppError> {
718 info!("Getting account information");
719 let result: AccountsResponse = self.http_client.get("accounts", Some(1)).await?;
720 debug!(
721 "Account information obtained: {} accounts",
722 result.accounts.len()
723 );
724 Ok(result)
725 }
726
727 async fn get_positions(&self) -> Result<PositionsResponse, AppError> {
728 debug!("Getting open positions");
729 let result: PositionsResponse = self.http_client.get("positions", Some(2)).await?;
730 debug!("Positions obtained: {} positions", result.positions.len());
731 Ok(result)
732 }
733
734 async fn get_positions_w_filter(&self, filter: &str) -> Result<PositionsResponse, AppError> {
735 debug!("Getting open positions with filter: {}", filter);
736 let mut positions = self.get_positions().await?;
737
738 positions
739 .positions
740 .retain(|position| position.market.epic.contains(filter));
741
742 debug!(
743 "Positions obtained after filtering: {} positions",
744 positions.positions.len()
745 );
746 Ok(positions)
747 }
748
749 async fn get_working_orders(&self) -> Result<WorkingOrdersResponse, AppError> {
750 info!("Getting working orders");
751 let result: WorkingOrdersResponse = self.http_client.get("workingorders", Some(2)).await?;
752 debug!(
753 "Working orders obtained: {} orders",
754 result.working_orders.len()
755 );
756 Ok(result)
757 }
758
759 async fn get_activity(
760 &self,
761 from: &str,
762 to: &str,
763 ) -> Result<AccountActivityResponse, AppError> {
764 let path = format!("history/activity?from={}&to={}&pageSize=500", from, to);
765 info!("Getting account activity");
766 let result: AccountActivityResponse = self.http_client.get(&path, Some(3)).await?;
767 debug!(
768 "Account activity obtained: {} activities",
769 result.activities.len()
770 );
771 Ok(result)
772 }
773
774 async fn get_activity_with_details(
775 &self,
776 from: &str,
777 to: &str,
778 ) -> Result<AccountActivityResponse, AppError> {
779 let path = format!(
780 "history/activity?from={}&to={}&detailed=true&pageSize=500",
781 from, to
782 );
783 info!("Getting detailed account activity");
784 let result: AccountActivityResponse = self.http_client.get(&path, Some(3)).await?;
785 debug!(
786 "Detailed account activity obtained: {} activities",
787 result.activities.len()
788 );
789 Ok(result)
790 }
791
792 async fn get_transactions(
793 &self,
794 from: &str,
795 to: &str,
796 ) -> Result<TransactionHistoryResponse, AppError> {
797 const PAGE_SIZE: u32 = 200;
798 let mut all_transactions = Vec::new();
799 let mut current_page = 1;
800 #[allow(unused_assignments)]
801 let mut last_metadata = None;
802
803 loop {
804 let path = format!(
805 "history/transactions?from={}&to={}&pageSize={}&pageNumber={}",
806 from, to, PAGE_SIZE, current_page
807 );
808 info!("Getting transaction history page {}", current_page);
809
810 let result: TransactionHistoryResponse = self.http_client.get(&path, Some(2)).await?;
811
812 let total_pages = result.metadata.page_data.total_pages as u32;
813 last_metadata = Some(result.metadata);
814 all_transactions.extend(result.transactions);
815
816 if current_page >= total_pages {
817 break;
818 }
819 current_page += 1;
820 }
821
822 debug!(
823 "Total transaction history obtained: {} transactions",
824 all_transactions.len()
825 );
826
827 Ok(TransactionHistoryResponse {
828 transactions: all_transactions,
829 metadata: last_metadata
830 .ok_or_else(|| AppError::InvalidInput("Could not retrieve metadata".to_string()))?,
831 })
832 }
833
834 async fn get_preferences(&self) -> Result<AccountPreferencesResponse, AppError> {
835 info!("Getting account preferences");
836 let result: AccountPreferencesResponse = self
837 .http_client
838 .get("accounts/preferences", Some(1))
839 .await?;
840 debug!(
841 "Account preferences obtained: trailing_stops_enabled={}",
842 result.trailing_stops_enabled
843 );
844 Ok(result)
845 }
846
847 async fn update_preferences(&self, trailing_stops_enabled: bool) -> Result<(), AppError> {
848 info!(
849 "Updating account preferences: trailing_stops_enabled={}",
850 trailing_stops_enabled
851 );
852 let request = serde_json::json!({
853 "trailingStopsEnabled": trailing_stops_enabled
854 });
855 let _: serde_json::Value = self
856 .http_client
857 .put("accounts/preferences", &request, Some(1))
858 .await?;
859 debug!("Account preferences updated");
860 Ok(())
861 }
862
863 async fn get_activity_by_period(
864 &self,
865 period_ms: u64,
866 ) -> Result<AccountActivityResponse, AppError> {
867 let path = format!("history/activity/{}", period_ms);
868 info!("Getting account activity for period: {} ms", period_ms);
869 let result: AccountActivityResponse = self.http_client.get(&path, Some(1)).await?;
870 debug!(
871 "Account activity obtained: {} activities",
872 result.activities.len()
873 );
874 Ok(result)
875 }
876}
877
878#[async_trait]
879impl OrderService for Client {
880 async fn create_order(
881 &self,
882 order: &CreateOrderRequest,
883 ) -> Result<CreateOrderResponse, AppError> {
884 info!("Creating order for: {}", order.epic);
885 let result: CreateOrderResponse = self
886 .http_client
887 .post("positions/otc", order, Some(2))
888 .await?;
889 debug!("Order created with reference: {}", result.deal_reference);
890 Ok(result)
891 }
892
893 async fn get_order_confirmation(
894 &self,
895 deal_reference: &str,
896 ) -> Result<OrderConfirmationResponse, AppError> {
897 let path = format!("confirms/{}", deal_reference);
898 info!("Getting confirmation for order: {}", deal_reference);
899 let result: OrderConfirmationResponse = self.http_client.get(&path, Some(1)).await?;
900 debug!("Confirmation obtained for order: {}", deal_reference);
901 Ok(result)
902 }
903
904 async fn get_order_confirmation_w_retry(
905 &self,
906 deal_reference: &str,
907 retries: u64,
908 delay_ms: u64,
909 ) -> Result<OrderConfirmationResponse, AppError> {
910 let base = Duration::from_millis(delay_ms);
913 let mut attempt: u32 = 0;
914 loop {
915 match self.get_order_confirmation(deal_reference).await {
916 Ok(response) => return Ok(response),
917 Err(e) => {
918 if !is_transient_confirmation_error(&e) {
921 return Err(e);
922 }
923 if u64::from(attempt) >= retries {
924 return Err(e);
925 }
926 let delay = backoff_delay(base, attempt);
927 let delay_ms = u64::try_from(delay.as_millis()).unwrap_or(u64::MAX);
928 let next_attempt = attempt.checked_add(1).ok_or_else(|| {
929 AppError::Generic("retry attempt counter overflow".to_string())
930 })?;
931 warn!(
932 deal_reference = %deal_reference,
933 attempt = next_attempt,
934 max_retries = retries,
935 delay_ms,
936 "retrying order confirmation after transient error"
937 );
938 sleep(delay).await;
939 attempt = next_attempt;
940 }
941 }
942 }
943 }
944
945 async fn update_position(
946 &self,
947 deal_id: &str,
948 update: &UpdatePositionRequest,
949 ) -> Result<UpdatePositionResponse, AppError> {
950 let path = format!("positions/otc/{}", deal_id);
951 info!("Updating position: {}", deal_id);
952 let result: UpdatePositionResponse = self.http_client.put(&path, update, Some(2)).await?;
953 debug!(
954 "Position updated: {} with deal reference: {}",
955 deal_id, result.deal_reference
956 );
957 Ok(result)
958 }
959
960 async fn update_level_in_position(
961 &self,
962 deal_id: &str,
963 limit_level: Option<f64>,
964 ) -> Result<UpdatePositionResponse, AppError> {
965 let path = format!("positions/otc/{}", deal_id);
966 info!("Updating position: {}", deal_id);
967 let limit_level = limit_level.unwrap_or(0.0);
968
969 let update: UpdatePositionRequest = UpdatePositionRequest {
970 guaranteed_stop: None,
971 limit_level: Some(limit_level),
972 stop_level: None,
973 trailing_stop: None,
974 trailing_stop_distance: None,
975 trailing_stop_increment: None,
976 };
977 let result: UpdatePositionResponse = self.http_client.put(&path, update, Some(2)).await?;
978 debug!(
979 "Position updated: {} with deal reference: {}",
980 deal_id, result.deal_reference
981 );
982 Ok(result)
983 }
984
985 async fn close_position(
986 &self,
987 close_request: &ClosePositionRequest,
988 ) -> Result<ClosePositionResponse, AppError> {
989 info!("Closing position");
990
991 let result: ClosePositionResponse = self
994 .http_client
995 .post_with_delete_method("positions/otc", close_request, Some(1))
996 .await?;
997
998 debug!("Position closed with reference: {}", result.deal_reference);
999 Ok(result)
1000 }
1001
1002 async fn create_working_order(
1003 &self,
1004 order: &CreateWorkingOrderRequest,
1005 ) -> Result<CreateWorkingOrderResponse, AppError> {
1006 info!("Creating working order for: {}", order.epic);
1007 let result: CreateWorkingOrderResponse = self
1008 .http_client
1009 .post("workingorders/otc", order, Some(2))
1010 .await?;
1011 debug!(
1012 "Working order created with reference: {}",
1013 result.deal_reference
1014 );
1015 Ok(result)
1016 }
1017
1018 async fn delete_working_order(&self, deal_id: &str) -> Result<(), AppError> {
1019 let path = format!("workingorders/otc/{}", deal_id);
1020 let result: CreateWorkingOrderResponse =
1021 self.http_client.delete(path.as_str(), Some(2)).await?;
1022 debug!(
1023 "Working order created with reference: {}",
1024 result.deal_reference
1025 );
1026 Ok(())
1027 }
1028
1029 async fn get_position(&self, deal_id: &str) -> Result<SinglePositionResponse, AppError> {
1030 let path = format!("positions/{}", deal_id);
1031 info!("Getting position: {}", deal_id);
1032 let result: SinglePositionResponse = self.http_client.get(&path, Some(2)).await?;
1033 debug!("Position obtained for deal: {}", deal_id);
1034 Ok(result)
1035 }
1036
1037 async fn update_working_order(
1038 &self,
1039 deal_id: &str,
1040 update: &UpdateWorkingOrderRequest,
1041 ) -> Result<CreateWorkingOrderResponse, AppError> {
1042 let path = format!("workingorders/otc/{}", deal_id);
1043 info!("Updating working order: {}", deal_id);
1044 let result: CreateWorkingOrderResponse =
1045 self.http_client.put(&path, update, Some(2)).await?;
1046 debug!(
1047 "Working order updated: {} with reference: {}",
1048 deal_id, result.deal_reference
1049 );
1050 Ok(result)
1051 }
1052}
1053
1054#[async_trait]
1059impl WatchlistService for Client {
1060 async fn get_watchlists(&self) -> Result<WatchlistsResponse, AppError> {
1061 info!("Getting all watchlists");
1062 let result: WatchlistsResponse = self.http_client.get("watchlists", Some(1)).await?;
1063 debug!(
1064 "Watchlists obtained: {} watchlists",
1065 result.watchlists.len()
1066 );
1067 Ok(result)
1068 }
1069
1070 async fn create_watchlist(
1071 &self,
1072 name: &str,
1073 epics: Option<&[String]>,
1074 ) -> Result<CreateWatchlistResponse, AppError> {
1075 info!("Creating watchlist: {}", name);
1076 let request = CreateWatchlistRequest {
1077 name: name.to_string(),
1078 epics: epics.map(|e| e.to_vec()),
1079 };
1080 let result: CreateWatchlistResponse = self
1081 .http_client
1082 .post("watchlists", &request, Some(1))
1083 .await?;
1084 debug!(
1085 "Watchlist created: {} with ID: {}",
1086 name, result.watchlist_id
1087 );
1088 Ok(result)
1089 }
1090
1091 async fn get_watchlist(
1092 &self,
1093 watchlist_id: &str,
1094 ) -> Result<WatchlistMarketsResponse, AppError> {
1095 let path = format!("watchlists/{}", watchlist_id);
1096 info!("Getting watchlist: {}", watchlist_id);
1097 let result: WatchlistMarketsResponse = self.http_client.get(&path, Some(1)).await?;
1098 debug!(
1099 "Watchlist obtained: {} with {} markets",
1100 watchlist_id,
1101 result.markets.len()
1102 );
1103 Ok(result)
1104 }
1105
1106 async fn delete_watchlist(&self, watchlist_id: &str) -> Result<StatusResponse, AppError> {
1107 let path = format!("watchlists/{}", watchlist_id);
1108 info!("Deleting watchlist: {}", watchlist_id);
1109 let result: StatusResponse = self.http_client.delete(&path, Some(1)).await?;
1110 debug!("Watchlist deleted: {}", watchlist_id);
1111 Ok(result)
1112 }
1113
1114 async fn add_to_watchlist(
1115 &self,
1116 watchlist_id: &str,
1117 epic: &str,
1118 ) -> Result<StatusResponse, AppError> {
1119 let path = format!("watchlists/{}", watchlist_id);
1120 info!("Adding {} to watchlist: {}", epic, watchlist_id);
1121 let request = AddToWatchlistRequest {
1122 epic: epic.to_string(),
1123 };
1124 let result: StatusResponse = self.http_client.put(&path, &request, Some(1)).await?;
1125 debug!("Added {} to watchlist: {}", epic, watchlist_id);
1126 Ok(result)
1127 }
1128
1129 async fn remove_from_watchlist(
1130 &self,
1131 watchlist_id: &str,
1132 epic: &str,
1133 ) -> Result<StatusResponse, AppError> {
1134 let path = format!("watchlists/{}/{}", watchlist_id, epic);
1135 info!("Removing {} from watchlist: {}", epic, watchlist_id);
1136 let result: StatusResponse = self.http_client.delete(&path, Some(1)).await?;
1137 debug!("Removed {} from watchlist: {}", epic, watchlist_id);
1138 Ok(result)
1139 }
1140}
1141
1142#[async_trait]
1147impl SentimentService for Client {
1148 async fn get_client_sentiment(
1149 &self,
1150 market_ids: &[String],
1151 ) -> Result<ClientSentimentResponse, AppError> {
1152 let market_ids_str = market_ids.join(",");
1153 let path = format!("clientsentiment?marketIds={}", market_ids_str);
1154 info!("Getting client sentiment for {} markets", market_ids.len());
1155 let result: ClientSentimentResponse = self.http_client.get(&path, Some(1)).await?;
1156 debug!(
1157 "Client sentiment obtained for {} markets",
1158 result.client_sentiments.len()
1159 );
1160 Ok(result)
1161 }
1162
1163 async fn get_client_sentiment_by_market(
1164 &self,
1165 market_id: &str,
1166 ) -> Result<MarketSentiment, AppError> {
1167 let path = format!("clientsentiment/{}", market_id);
1168 info!("Getting client sentiment for market: {}", market_id);
1169 let result: MarketSentiment = self.http_client.get(&path, Some(1)).await?;
1170 debug!(
1171 "Client sentiment for {}: {}% long, {}% short",
1172 market_id, result.long_position_percentage, result.short_position_percentage
1173 );
1174 Ok(result)
1175 }
1176
1177 async fn get_related_sentiment(
1178 &self,
1179 market_id: &str,
1180 ) -> Result<ClientSentimentResponse, AppError> {
1181 let path = format!("clientsentiment/related/{}", market_id);
1182 info!("Getting related sentiment for market: {}", market_id);
1183 let result: ClientSentimentResponse = self.http_client.get(&path, Some(1)).await?;
1184 debug!(
1185 "Related sentiment obtained: {} markets",
1186 result.client_sentiments.len()
1187 );
1188 Ok(result)
1189 }
1190}
1191
1192#[async_trait]
1197impl CostsService for Client {
1198 async fn get_indicative_costs_open(
1199 &self,
1200 request: &OpenCostsRequest,
1201 ) -> Result<IndicativeCostsResponse, AppError> {
1202 info!(
1203 "Getting indicative costs for opening position on: {}",
1204 request.epic
1205 );
1206 let result: IndicativeCostsResponse = self
1207 .http_client
1208 .post("indicativecostsandcharges/open", request, Some(1))
1209 .await?;
1210 debug!(
1211 "Indicative costs obtained, reference: {}",
1212 result.indicative_quote_reference
1213 );
1214 Ok(result)
1215 }
1216
1217 async fn get_indicative_costs_close(
1218 &self,
1219 request: &CloseCostsRequest,
1220 ) -> Result<IndicativeCostsResponse, AppError> {
1221 info!(
1222 "Getting indicative costs for closing position: {}",
1223 request.deal_id
1224 );
1225 let result: IndicativeCostsResponse = self
1226 .http_client
1227 .post("indicativecostsandcharges/close", request, Some(1))
1228 .await?;
1229 debug!(
1230 "Indicative costs obtained, reference: {}",
1231 result.indicative_quote_reference
1232 );
1233 Ok(result)
1234 }
1235
1236 async fn get_indicative_costs_edit(
1237 &self,
1238 request: &EditCostsRequest,
1239 ) -> Result<IndicativeCostsResponse, AppError> {
1240 info!(
1241 "Getting indicative costs for editing position: {}",
1242 request.deal_id
1243 );
1244 let result: IndicativeCostsResponse = self
1245 .http_client
1246 .post("indicativecostsandcharges/edit", request, Some(1))
1247 .await?;
1248 debug!(
1249 "Indicative costs obtained, reference: {}",
1250 result.indicative_quote_reference
1251 );
1252 Ok(result)
1253 }
1254
1255 async fn get_costs_history(
1256 &self,
1257 from: &str,
1258 to: &str,
1259 ) -> Result<CostsHistoryResponse, AppError> {
1260 let path = format!("indicativecostsandcharges/history/from/{}/to/{}", from, to);
1261 info!("Getting costs history from {} to {}", from, to);
1262 let result: CostsHistoryResponse = self.http_client.get(&path, Some(1)).await?;
1263 debug!("Costs history obtained: {} entries", result.costs.len());
1264 Ok(result)
1265 }
1266
1267 async fn get_durable_medium(
1268 &self,
1269 quote_reference: &str,
1270 ) -> Result<DurableMediumResponse, AppError> {
1271 let path = format!(
1272 "indicativecostsandcharges/durablemedium/{}",
1273 quote_reference
1274 );
1275 info!("Getting durable medium for reference: {}", quote_reference);
1276 let result: DurableMediumResponse = self.http_client.get(&path, Some(1)).await?;
1277 debug!("Durable medium obtained for reference: {}", quote_reference);
1278 Ok(result)
1279 }
1280}
1281
1282#[async_trait]
1287impl OperationsService for Client {
1288 async fn get_client_apps(&self) -> Result<ApplicationDetailsResponse, AppError> {
1289 info!("Getting client applications");
1290 let result: ApplicationDetailsResponse = self
1291 .http_client
1292 .get("operations/application", Some(1))
1293 .await?;
1294 debug!(
1296 name = ?result.name,
1297 status = %result.status,
1298 "Client application obtained"
1299 );
1300 Ok(result)
1301 }
1302
1303 async fn disable_client_app(&self) -> Result<StatusResponse, AppError> {
1304 info!("Disabling current client application");
1305 let result: StatusResponse = self
1306 .http_client
1307 .put(
1308 "operations/application/disable",
1309 &serde_json::json!({}),
1310 Some(1),
1311 )
1312 .await?;
1313 debug!("Client application disabled");
1314 Ok(result)
1315 }
1316}
1317
1318pub struct StreamerClient {
1328 account_id: String,
1329 market_streamer_client: Option<Arc<Mutex<LightstreamerClient>>>,
1330 price_streamer_client: Option<Arc<Mutex<LightstreamerClient>>>,
1331 has_market_stream_subs: bool,
1333 has_price_stream_subs: bool,
1334 converter_tasks: Vec<JoinHandle<()>>,
1340}
1341
1342impl StreamerClient {
1343 pub async fn new() -> Result<Self, AppError> {
1358 let client = Client::try_new()?;
1359 Self::with_client(&client).await
1360 }
1361
1362 pub async fn with_client(client: &Client) -> Result<Self, AppError> {
1376 let ws_info = client.ws_info().await?;
1377 let password = ws_info.get_ws_password();
1378
1379 let market_streamer_client = Arc::new(Mutex::new(LightstreamerClient::new(
1381 Some(ws_info.server.as_str()),
1382 None,
1383 Some(&ws_info.account_id),
1384 Some(&password),
1385 )?));
1386
1387 let price_streamer_client = Arc::new(Mutex::new(LightstreamerClient::new(
1388 Some(ws_info.server.as_str()),
1389 None,
1390 Some(&ws_info.account_id),
1391 Some(&password),
1392 )?));
1393
1394 {
1397 let mut streamer = market_streamer_client.lock().await;
1398 streamer
1399 .connection_options
1400 .set_forced_transport(Some(Transport::WsStreaming));
1401 streamer.set_logging_type(LogType::TracingLogs);
1402 }
1403 {
1404 let mut streamer = price_streamer_client.lock().await;
1405 streamer
1406 .connection_options
1407 .set_forced_transport(Some(Transport::WsStreaming));
1408 streamer.set_logging_type(LogType::TracingLogs);
1409 }
1410
1411 Ok(Self {
1412 account_id: ws_info.account_id.clone(),
1413 market_streamer_client: Some(market_streamer_client),
1414 price_streamer_client: Some(price_streamer_client),
1415 has_market_stream_subs: false,
1416 has_price_stream_subs: false,
1417 converter_tasks: Vec::new(),
1418 })
1419 }
1420
1421 pub async fn market_subscribe(
1451 &mut self,
1452 epics: Vec<String>,
1453 fields: HashSet<StreamingMarketField>,
1454 ) -> Result<mpsc::UnboundedReceiver<PriceData>, AppError> {
1455 self.has_market_stream_subs = true;
1457
1458 let fields = get_streaming_market_fields(&fields);
1459 let market_epics: Vec<String> = epics
1460 .iter()
1461 .map(|epic| "MARKET:".to_string() + epic)
1462 .collect();
1463 let mut subscription =
1464 Subscription::new(SubscriptionMode::Merge, Some(market_epics), Some(fields))?;
1465
1466 subscription.set_data_adapter(None)?;
1467 subscription.set_requested_snapshot(Some(Snapshot::Yes))?;
1468
1469 let (listener, item_receiver) = ChannelSubscriptionListener::create_channel();
1471 subscription.add_listener(Box::new(listener));
1472
1473 let client = self.market_streamer_client.as_ref().ok_or_else(|| {
1475 AppError::WebSocketError("market streamer client not initialized".to_string())
1476 })?;
1477
1478 {
1479 let mut client = client.lock().await;
1480 client
1481 .connection_options
1482 .set_forced_transport(Some(Transport::WsStreaming));
1483 LightstreamerClient::subscribe(client.subscription_sender.clone(), subscription)
1484 .await?;
1485 }
1486
1487 let (price_tx, price_rx) = mpsc::unbounded_channel();
1489 let handle = tokio::spawn(async move {
1490 let mut receiver = item_receiver;
1491 while let Some(item_update) = receiver.recv().await {
1492 let price_data = PriceData::from(&item_update);
1493 if price_tx.send(price_data).is_err() {
1494 tracing::debug!("Price channel receiver dropped");
1495 break;
1496 }
1497 }
1498 });
1499 self.converter_tasks.push(handle);
1501
1502 info!(
1503 "Market subscription created for {} instruments",
1504 epics.len()
1505 );
1506 Ok(price_rx)
1507 }
1508
1509 pub async fn trade_subscribe(
1532 &mut self,
1533 ) -> Result<mpsc::UnboundedReceiver<TradeFields>, AppError> {
1534 self.has_market_stream_subs = true;
1536
1537 let account_id = self.account_id.clone();
1538 let fields = Some(vec![
1539 "CONFIRMS".to_string(),
1540 "OPU".to_string(),
1541 "WOU".to_string(),
1542 ]);
1543 let trade_items = vec![format!("TRADE:{account_id}")];
1544
1545 let mut subscription =
1546 Subscription::new(SubscriptionMode::Distinct, Some(trade_items), fields)?;
1547
1548 subscription.set_data_adapter(None)?;
1549 subscription.set_requested_snapshot(Some(Snapshot::Yes))?;
1550
1551 let (listener, item_receiver) = ChannelSubscriptionListener::create_channel();
1553 subscription.add_listener(Box::new(listener));
1554
1555 let client = self.market_streamer_client.as_ref().ok_or_else(|| {
1557 AppError::WebSocketError("market streamer client not initialized".to_string())
1558 })?;
1559
1560 {
1561 let mut client = client.lock().await;
1562 client
1563 .connection_options
1564 .set_forced_transport(Some(Transport::WsStreaming));
1565 LightstreamerClient::subscribe(client.subscription_sender.clone(), subscription)
1566 .await?;
1567 }
1568
1569 let (trade_tx, trade_rx) = mpsc::unbounded_channel();
1571 let handle = tokio::spawn(async move {
1572 let mut receiver = item_receiver;
1573 while let Some(item_update) = receiver.recv().await {
1574 let trade_data = crate::presentation::trade::TradeData::from(&item_update);
1575 if trade_tx.send(trade_data.fields).is_err() {
1576 tracing::debug!("Trade channel receiver dropped");
1577 break;
1578 }
1579 }
1580 });
1581 self.converter_tasks.push(handle);
1583
1584 info!("Trade subscription created for account: {}", account_id);
1585 Ok(trade_rx)
1586 }
1587
1588 pub async fn account_subscribe(
1615 &mut self,
1616 fields: HashSet<StreamingAccountDataField>,
1617 ) -> Result<mpsc::UnboundedReceiver<AccountFields>, AppError> {
1618 self.has_market_stream_subs = true;
1620
1621 let fields = get_streaming_account_data_fields(&fields);
1622 let account_id = self.account_id.clone();
1623 let account_items = vec![format!("ACCOUNT:{account_id}")];
1624
1625 let mut subscription =
1626 Subscription::new(SubscriptionMode::Merge, Some(account_items), Some(fields))?;
1627
1628 subscription.set_data_adapter(None)?;
1629 subscription.set_requested_snapshot(Some(Snapshot::Yes))?;
1630
1631 let (listener, item_receiver) = ChannelSubscriptionListener::create_channel();
1633 subscription.add_listener(Box::new(listener));
1634
1635 let client = self.market_streamer_client.as_ref().ok_or_else(|| {
1637 AppError::WebSocketError("market streamer client not initialized".to_string())
1638 })?;
1639
1640 {
1641 let mut client = client.lock().await;
1642 client
1643 .connection_options
1644 .set_forced_transport(Some(Transport::WsStreaming));
1645 LightstreamerClient::subscribe(client.subscription_sender.clone(), subscription)
1646 .await?;
1647 }
1648
1649 let (account_tx, account_rx) = mpsc::unbounded_channel();
1651 let handle = tokio::spawn(async move {
1652 let mut receiver = item_receiver;
1653 while let Some(item_update) = receiver.recv().await {
1654 let account_data = crate::presentation::account::AccountData::from(&item_update);
1655 if account_tx.send(account_data.fields).is_err() {
1656 tracing::debug!("Account channel receiver dropped");
1657 break;
1658 }
1659 }
1660 });
1661 self.converter_tasks.push(handle);
1663
1664 info!("Account subscription created for account: {}", account_id);
1665 Ok(account_rx)
1666 }
1667
1668 pub async fn price_subscribe(
1699 &mut self,
1700 epics: Vec<String>,
1701 fields: HashSet<StreamingPriceField>,
1702 ) -> Result<mpsc::UnboundedReceiver<PriceData>, AppError> {
1703 self.has_price_stream_subs = true;
1705
1706 let fields = get_streaming_price_fields(&fields);
1707 let account_id = self.account_id.clone();
1708 let price_epics: Vec<String> = epics
1709 .iter()
1710 .map(|epic| format!("PRICE:{account_id}:{epic}"))
1711 .collect();
1712
1713 tracing::debug!("Pricing subscribe items: {:?}", price_epics);
1715 tracing::debug!("Pricing subscribe fields: {:?}", fields);
1716
1717 let mut subscription =
1718 Subscription::new(SubscriptionMode::Merge, Some(price_epics), Some(fields))?;
1719
1720 let pricing_adapter =
1722 std::env::var("IG_PRICING_ADAPTER").unwrap_or_else(|_| "Pricing".to_string());
1723 tracing::debug!("Using Pricing data adapter: {}", pricing_adapter);
1724 subscription.set_data_adapter(Some(pricing_adapter))?;
1725 subscription.set_requested_snapshot(Some(Snapshot::Yes))?;
1726
1727 let (listener, item_receiver) = ChannelSubscriptionListener::create_channel();
1729 subscription.add_listener(Box::new(listener));
1730
1731 let client = self.price_streamer_client.as_ref().ok_or_else(|| {
1733 AppError::WebSocketError("price streamer client not initialized".to_string())
1734 })?;
1735
1736 {
1737 let mut client = client.lock().await;
1738 client
1739 .connection_options
1740 .set_forced_transport(Some(Transport::WsStreaming));
1741 LightstreamerClient::subscribe(client.subscription_sender.clone(), subscription)
1742 .await?;
1743 }
1744
1745 let (price_tx, price_rx) = mpsc::unbounded_channel();
1747 let handle = tokio::spawn(async move {
1748 let mut receiver = item_receiver;
1749 while let Some(item_update) = receiver.recv().await {
1750 let price_data = PriceData::from(&item_update);
1751 if price_tx.send(price_data).is_err() {
1752 tracing::debug!("Price channel receiver dropped");
1753 break;
1754 }
1755 }
1756 });
1757 self.converter_tasks.push(handle);
1759
1760 info!(
1761 "Price subscription created for {} instruments (account: {})",
1762 epics.len(),
1763 account_id
1764 );
1765 Ok(price_rx)
1766 }
1767
1768 pub async fn chart_subscribe(
1801 &mut self,
1802 epics: Vec<String>,
1803 scale: ChartScale,
1804 fields: HashSet<StreamingChartField>,
1805 ) -> Result<mpsc::UnboundedReceiver<ChartData>, AppError> {
1806 self.has_market_stream_subs = true;
1808
1809 let fields = get_streaming_chart_fields(&fields);
1810
1811 let chart_items: Vec<String> = epics
1812 .iter()
1813 .map(|epic| format!("CHART:{epic}:{scale}",))
1814 .collect();
1815
1816 let mode = if matches!(scale, ChartScale::Tick) {
1818 SubscriptionMode::Distinct
1819 } else {
1820 SubscriptionMode::Merge
1821 };
1822
1823 let mut subscription = Subscription::new(mode, Some(chart_items), Some(fields))?;
1824
1825 subscription.set_data_adapter(None)?;
1826 subscription.set_requested_snapshot(Some(Snapshot::Yes))?;
1827
1828 let (listener, item_receiver) = ChannelSubscriptionListener::create_channel();
1830 subscription.add_listener(Box::new(listener));
1831
1832 let client = self.market_streamer_client.as_ref().ok_or_else(|| {
1834 AppError::WebSocketError("market streamer client not initialized".to_string())
1835 })?;
1836
1837 {
1838 let mut client = client.lock().await;
1839 client
1840 .connection_options
1841 .set_forced_transport(Some(Transport::WsStreaming));
1842 LightstreamerClient::subscribe(client.subscription_sender.clone(), subscription)
1843 .await?;
1844 }
1845
1846 let (chart_tx, chart_rx) = mpsc::unbounded_channel();
1848 let handle = tokio::spawn(async move {
1849 let mut receiver = item_receiver;
1850 while let Some(item_update) = receiver.recv().await {
1851 let chart_data = ChartData::from(&item_update);
1852 if chart_tx.send(chart_data).is_err() {
1853 tracing::debug!("Chart channel receiver dropped");
1854 break;
1855 }
1856 }
1857 });
1858 self.converter_tasks.push(handle);
1860
1861 info!(
1862 "Chart subscription created for {} instruments (scale: {})",
1863 epics.len(),
1864 scale
1865 );
1866
1867 Ok(chart_rx)
1868 }
1869
1870 pub async fn connect(&mut self, shutdown_signal: Option<Arc<Notify>>) -> Result<(), AppError> {
1887 let signal = if let Some(sig) = shutdown_signal {
1889 sig
1890 } else {
1891 let sig = Arc::new(Notify::new());
1892 setup_signal_hook(Arc::clone(&sig)).await;
1893 sig
1894 };
1895
1896 let mut tasks = Vec::new();
1897 let mut connection_signals: Vec<Arc<Notify>> = Vec::new();
1902
1903 if self.has_market_stream_subs {
1905 if let Some(client) = self.market_streamer_client.as_ref() {
1906 let client = Arc::clone(client);
1907 let conn_signal = Arc::new(Notify::new());
1908 connection_signals.push(Arc::clone(&conn_signal));
1909 let task = tokio::spawn(async move {
1910 Self::connect_client(client, conn_signal, "Market").await
1911 });
1912 tasks.push(task);
1913 }
1914 } else {
1915 info!("Skipping Market streamer connection: no active subscriptions");
1916 }
1917
1918 if self.has_price_stream_subs {
1920 if let Some(client) = self.price_streamer_client.as_ref() {
1921 let client = Arc::clone(client);
1922 let conn_signal = Arc::new(Notify::new());
1923 connection_signals.push(Arc::clone(&conn_signal));
1924 let task = tokio::spawn(async move {
1925 Self::connect_client(client, conn_signal, "Price").await
1926 });
1927 tasks.push(task);
1928 }
1929 } else {
1930 info!("Skipping Price streamer connection: no active subscriptions");
1931 }
1932
1933 if tasks.is_empty() {
1934 warn!("No streaming clients selected for connection (no active subscriptions)");
1935 return Ok(());
1936 }
1937
1938 info!("Connecting {} streaming client(s)...", tasks.len());
1939
1940 let fanout = spawn_shutdown_fanout(Arc::clone(&signal), connection_signals);
1944
1945 let results = futures::future::join_all(tasks).await;
1947
1948 fanout.abort();
1951
1952 let mut has_error = false;
1954 for (idx, result) in results.iter().enumerate() {
1955 match result {
1956 Ok(Ok(_)) => {
1957 debug!("Streaming client {} completed successfully", idx);
1958 }
1959 Ok(Err(e)) => {
1960 error!("Streaming client {} failed: {:?}", idx, e);
1961 has_error = true;
1962 }
1963 Err(e) => {
1964 error!("Streaming client {} task panicked: {:?}", idx, e);
1965 has_error = true;
1966 }
1967 }
1968 }
1969
1970 if has_error {
1971 return Err(AppError::WebSocketError(
1972 "one or more streaming connections failed".to_string(),
1973 ));
1974 }
1975
1976 info!("All streaming connections closed gracefully");
1977 Ok(())
1978 }
1979
1980 async fn connect_client(
1982 client: Arc<Mutex<LightstreamerClient>>,
1983 signal: Arc<Notify>,
1984 client_type: &str,
1985 ) -> Result<(), AppError> {
1986 let mut retry_interval_millis: u64 = 0;
1987 let mut retry_counter: u64 = 0;
1988
1989 while retry_counter < MAX_CONNECTION_ATTEMPTS {
1990 let connect_result = {
1991 let mut client = client.lock().await;
1992 client.connect_direct(Arc::clone(&signal)).await
1993 };
1994
1995 match connect_result {
1996 Ok(()) => {
1997 info!("{} streamer connected successfully", client_type);
1998 break;
1999 }
2000 Err(e) => {
2001 if is_graceful_close(&e) {
2006 info!(
2007 "{} streamer closed gracefully: no active subscriptions (server reason: {})",
2008 client_type, GRACEFUL_CLOSE_MARKER
2009 );
2010 return Ok(());
2011 }
2012
2013 let error_msg = e.to_string();
2017 error!("{} streamer connection failed: {}", client_type, error_msg);
2018
2019 if retry_counter < MAX_CONNECTION_ATTEMPTS - 1 {
2020 sleep(Duration::from_millis(retry_interval_millis)).await;
2021 retry_interval_millis =
2022 (retry_interval_millis + (200 * retry_counter)).min(5000);
2023 retry_counter += 1;
2024 warn!(
2025 "{} streamer retrying (attempt {}/{}) in {:.2} seconds...",
2026 client_type,
2027 retry_counter + 1,
2028 MAX_CONNECTION_ATTEMPTS,
2029 retry_interval_millis as f64 / 1000.0
2030 );
2031 } else {
2032 retry_counter += 1;
2033 }
2034 }
2035 }
2036 }
2037
2038 if retry_counter >= MAX_CONNECTION_ATTEMPTS {
2039 error!(
2040 "{} streamer failed after {} attempts",
2041 client_type, MAX_CONNECTION_ATTEMPTS
2042 );
2043 return Err(AppError::WebSocketError(format!(
2044 "{} streamer: maximum connection attempts ({}) exceeded",
2045 client_type, MAX_CONNECTION_ATTEMPTS
2046 )));
2047 }
2048
2049 info!("{} streamer connection closed gracefully", client_type);
2050 Ok(())
2051 }
2052
2053 pub async fn disconnect(&mut self) -> Result<(), AppError> {
2067 let mut disconnected = 0;
2068
2069 if let Some(client) = self.market_streamer_client.as_ref() {
2070 let mut client = client.lock().await;
2071 client.disconnect().await;
2072 info!("Market streamer disconnected");
2073 disconnected += 1;
2074 }
2075
2076 if let Some(client) = self.price_streamer_client.as_ref() {
2077 let mut client = client.lock().await;
2078 client.disconnect().await;
2079 info!("Price streamer disconnected");
2080 disconnected += 1;
2081 }
2082
2083 let converter_count = self.converter_tasks.len();
2086 abort_and_drain_tasks(&mut self.converter_tasks).await;
2087 if converter_count > 0 {
2088 debug!("Aborted {} converter task(s)", converter_count);
2089 }
2090
2091 info!("Disconnected {} streaming client(s)", disconnected);
2092 Ok(())
2093 }
2094}
2095
2096impl Drop for StreamerClient {
2097 fn drop(&mut self) {
2101 for handle in self.converter_tasks.drain(..) {
2102 handle.abort();
2103 }
2104 }
2105}
2106
2107#[cfg(test)]
2108mod tests {
2109 use super::is_transient_confirmation_error;
2110 use crate::error::AppError;
2111 use reqwest::StatusCode;
2112
2113 #[test]
2114 fn test_confirmation_error_rate_limit_is_transient() {
2115 assert!(is_transient_confirmation_error(
2116 &AppError::RateLimitExceeded
2117 ));
2118 }
2119
2120 #[test]
2121 fn test_confirmation_error_not_found_is_transient() {
2122 assert!(is_transient_confirmation_error(&AppError::NotFound));
2124 assert!(is_transient_confirmation_error(&AppError::Unexpected(
2125 StatusCode::NOT_FOUND
2126 )));
2127 }
2128
2129 #[test]
2130 fn test_confirmation_error_server_error_is_transient() {
2131 assert!(is_transient_confirmation_error(&AppError::Unexpected(
2132 StatusCode::INTERNAL_SERVER_ERROR
2133 )));
2134 assert!(is_transient_confirmation_error(&AppError::Unexpected(
2135 StatusCode::BAD_GATEWAY
2136 )));
2137 }
2138
2139 #[test]
2140 fn test_confirmation_error_invalid_input_is_permanent() {
2141 assert!(!is_transient_confirmation_error(&AppError::InvalidInput(
2142 "bad".to_string()
2143 )));
2144 }
2145
2146 #[test]
2147 fn test_confirmation_error_auth_and_deser_are_permanent() {
2148 assert!(!is_transient_confirmation_error(&AppError::Unauthorized));
2149 assert!(!is_transient_confirmation_error(
2150 &AppError::OAuthTokenExpired
2151 ));
2152 assert!(!is_transient_confirmation_error(
2153 &AppError::Deserialization("bad".to_string())
2154 ));
2155 assert!(!is_transient_confirmation_error(&AppError::Unexpected(
2156 StatusCode::BAD_REQUEST
2157 )));
2158 }
2159
2160 use super::{
2161 GRACEFUL_CLOSE_MARKER, abort_and_drain_tasks, is_graceful_close, spawn_shutdown_fanout,
2162 };
2163 use lightstreamer_rs::utils::LightstreamerError;
2164 use std::sync::Arc;
2165 use std::time::Duration;
2166 use tokio::sync::Notify;
2167 use tokio::task::JoinHandle;
2168
2169 #[test]
2172 fn test_is_graceful_close_connection_variant_with_marker_is_true() {
2173 let err = LightstreamerError::Connection(format!(
2176 "connection error from server: conerr,-2,{GRACEFUL_CLOSE_MARKER}"
2177 ));
2178 assert!(is_graceful_close(&err));
2179 }
2180
2181 #[test]
2182 fn test_is_graceful_close_protocol_and_invalid_state_with_marker_is_true() {
2183 let protocol = LightstreamerError::Protocol(format!("closing: {GRACEFUL_CLOSE_MARKER}"));
2184 let invalid_state =
2185 LightstreamerError::InvalidState(format!("state: {GRACEFUL_CLOSE_MARKER}"));
2186 assert!(is_graceful_close(&protocol));
2187 assert!(is_graceful_close(&invalid_state));
2188 }
2189
2190 #[test]
2191 fn test_is_graceful_close_connection_variant_without_marker_is_false() {
2192 let err = LightstreamerError::Connection("no message received within 5000 ms".to_string());
2193 assert!(!is_graceful_close(&err));
2194 }
2195
2196 #[test]
2197 fn test_is_graceful_close_other_variant_with_marker_is_false() {
2198 let err = LightstreamerError::Timeout(GRACEFUL_CLOSE_MARKER.to_string());
2201 assert!(!is_graceful_close(&err));
2202 }
2203
2204 #[tokio::test]
2207 async fn test_shutdown_fanout_wakes_all_connection_waiters() {
2208 let source = Arc::new(Notify::new());
2211 let market_signal = Arc::new(Notify::new());
2212 let price_signal = Arc::new(Notify::new());
2213
2214 let fanout = spawn_shutdown_fanout(
2215 Arc::clone(&source),
2216 vec![Arc::clone(&market_signal), Arc::clone(&price_signal)],
2217 );
2218
2219 let market_waiter = tokio::spawn(async move { market_signal.notified().await });
2220 let price_waiter = tokio::spawn(async move { price_signal.notified().await });
2221
2222 source.notify_one();
2227
2228 assert!(
2229 tokio::time::timeout(Duration::from_secs(1), market_waiter)
2230 .await
2231 .is_ok(),
2232 "market connection did not observe the shutdown signal"
2233 );
2234 assert!(
2235 tokio::time::timeout(Duration::from_secs(1), price_waiter)
2236 .await
2237 .is_ok(),
2238 "price connection did not observe the shutdown signal"
2239 );
2240
2241 fanout.abort();
2242 }
2243
2244 #[tokio::test]
2247 async fn test_abort_and_drain_tasks_stops_and_clears() {
2248 let mut tasks: Vec<JoinHandle<()>> = Vec::new();
2249 for _ in 0..3 {
2250 tasks.push(tokio::spawn(async { std::future::pending::<()>().await }));
2252 }
2253 assert!(tasks.iter().all(|h| !h.is_finished()));
2254
2255 abort_and_drain_tasks(&mut tasks).await;
2256
2257 assert!(tasks.is_empty());
2260 }
2261
2262 #[tokio::test]
2263 async fn test_abort_makes_pending_task_finish() {
2264 let handle: JoinHandle<()> = tokio::spawn(async { std::future::pending::<()>().await });
2265 assert!(!handle.is_finished());
2266 handle.abort();
2267 let join_result = handle.await;
2268 assert!(
2269 join_result.is_err(),
2270 "aborted task should yield a cancellation JoinError"
2271 );
2272 }
2273}