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;
41#[cfg(feature = "streaming")]
42use crate::model::streaming::{
43 StreamingAccountDataField, StreamingChartField, StreamingMarketField, StreamingPriceField,
44 get_streaming_account_data_fields, get_streaming_chart_fields, get_streaming_market_fields,
45 get_streaming_price_fields,
46};
47#[cfg(feature = "streaming")]
48use crate::presentation::account::AccountFields;
49#[cfg(feature = "streaming")]
50use crate::presentation::chart::{ChartData, ChartScale};
51use crate::presentation::market::{MarketData, MarketDetails};
52#[cfg(feature = "streaming")]
53use crate::presentation::price::PriceData;
54#[cfg(feature = "streaming")]
55use crate::presentation::trade::TradeFields;
56use async_trait::async_trait;
57use futures::StreamExt;
58#[cfg(feature = "streaming")]
59use lightstreamer_rs::client::{LightstreamerClient, LogType, Transport};
60#[cfg(feature = "streaming")]
61use lightstreamer_rs::subscription::{
62 ChannelSubscriptionListener, Snapshot, Subscription, SubscriptionMode,
63};
64#[cfg(feature = "streaming")]
65use lightstreamer_rs::utils::{LightstreamerError, setup_signal_hook};
66use reqwest::StatusCode;
67use std::collections::HashSet;
68use std::sync::Arc;
69use std::time::Duration;
70#[cfg(feature = "streaming")]
71use tokio::sync::{Mutex, Notify, mpsc};
72#[cfg(feature = "streaming")]
73use tokio::task::JoinHandle;
74use tokio::time::sleep;
75#[cfg(feature = "streaming")]
76use tracing::error;
77use tracing::{debug, info, warn};
78
79#[cfg(feature = "streaming")]
80const MAX_CONNECTION_ATTEMPTS: u64 = 3;
81
82const MARKET_DETAILS_CONCURRENCY: usize = 6;
88
89#[cfg(feature = "streaming")]
96const GRACEFUL_CLOSE_MARKER: &str = "No more requests to fulfill";
97
98#[cfg(feature = "streaming")]
110#[must_use]
111#[inline]
112fn is_graceful_close(error: &LightstreamerError) -> bool {
113 match error {
114 LightstreamerError::Connection(msg)
115 | LightstreamerError::Protocol(msg)
116 | LightstreamerError::InvalidState(msg) => msg.contains(GRACEFUL_CLOSE_MARKER),
117 _ => false,
118 }
119}
120
121#[cfg(feature = "streaming")]
137#[must_use]
138fn spawn_shutdown_fanout(source: Arc<Notify>, targets: Vec<Arc<Notify>>) -> JoinHandle<()> {
139 tokio::spawn(async move {
140 source.notified().await;
141 for target in &targets {
142 target.notify_one();
143 }
144 })
145}
146
147#[cfg(feature = "streaming")]
154async fn abort_and_drain_tasks(tasks: &mut Vec<JoinHandle<()>>) {
155 for handle in tasks.drain(..) {
156 handle.abort();
157 let _ = handle.await;
159 }
160}
161
162#[must_use]
178fn is_transient_confirmation_error(err: &AppError) -> bool {
179 match err {
180 AppError::RateLimitExceeded | AppError::Network(_) | AppError::NotFound => true,
181 AppError::Unexpected(status) => {
182 *status == StatusCode::NOT_FOUND || status.is_server_error()
183 }
184 _ => false,
185 }
186}
187
188pub struct Client {
193 http_client: Arc<HttpClient>,
194}
195
196impl Client {
197 pub fn try_new() -> Result<Self, AppError> {
215 let http_client = Arc::new(HttpClient::new_lazy(Config::default())?);
216 Ok(Self { http_client })
217 }
218
219 pub fn with_config(config: Config) -> Result<Self, AppError> {
281 let http_client = Arc::new(HttpClient::new_lazy(config)?);
282 Ok(Self { http_client })
283 }
284
285 #[inline]
293 #[must_use]
294 pub fn config(&self) -> &Config {
295 self.http_client.config()
296 }
297
298 pub async fn ws_info(&self) -> Result<WebsocketInfo, AppError> {
312 self.http_client.ws_info().await
313 }
314
315 #[deprecated(
320 note = "use ws_info() which reuses the cached session and returns a typed error instead of a default-on-error WebsocketInfo"
321 )]
322 pub async fn get_ws_info(&self) -> WebsocketInfo {
323 self.ws_info().await.unwrap_or_default()
324 }
325}
326
327#[async_trait]
328impl MarketService for Client {
329 async fn search_markets(&self, search_term: &str) -> Result<MarketSearchResponse, AppError> {
330 let path = format!("markets?searchTerm={}", search_term);
331 info!("Searching markets with term: {}", search_term);
332 let result: MarketSearchResponse = self.http_client.get(&path, Some(1)).await?;
333 debug!("{} markets found", result.markets.len());
334 Ok(result)
335 }
336
337 async fn get_market_details(&self, epic: &str) -> Result<MarketDetails, AppError> {
338 let path = format!("markets/{epic}");
339 info!("Getting market details: {}", epic);
340 let market_details: MarketDetails = self.http_client.get(&path, Some(3)).await?;
344 debug!("Market details obtained for: {}", epic);
345 Ok(market_details)
346 }
347
348 async fn get_multiple_market_details(
349 &self,
350 epics: &[String],
351 ) -> Result<MultipleMarketDetailsResponse, AppError> {
352 if epics.is_empty() {
353 return Ok(MultipleMarketDetailsResponse::default());
354 } else if epics.len() > 50 {
355 return Err(AppError::InvalidInput(
356 "The maximum number of EPICs is 50".to_string(),
357 ));
358 }
359
360 let epics_str = epics.join(",");
361 let path = format!("markets?epics={}", epics_str);
362 debug!(
363 "Getting market details for {} EPICs in a batch",
364 epics.len()
365 );
366
367 let response: MultipleMarketDetailsResponse = self.http_client.get(&path, Some(2)).await?;
368
369 Ok(response)
370 }
371
372 async fn get_historical_prices(
373 &self,
374 epic: &str,
375 resolution: &str,
376 from: &str,
377 to: &str,
378 ) -> Result<HistoricalPricesResponse, AppError> {
379 let path = format!(
380 "prices/{}?resolution={}&from={}&to={}",
381 epic, resolution, from, to
382 );
383 info!("Getting historical prices for: {}", epic);
384 let result: HistoricalPricesResponse = self.http_client.get(&path, Some(3)).await?;
385 debug!("Historical prices obtained for: {}", epic);
386 Ok(result)
387 }
388
389 async fn get_historical_prices_by_date_range(
390 &self,
391 epic: &str,
392 resolution: &str,
393 start_date: &str,
394 end_date: &str,
395 ) -> Result<HistoricalPricesResponse, AppError> {
396 let path = format!("prices/{}/{}/{}/{}", epic, resolution, start_date, end_date);
397 info!(
398 "Getting historical prices for epic: {}, resolution: {}, from: {} to: {}",
399 epic, resolution, start_date, end_date
400 );
401 let result: HistoricalPricesResponse = self.http_client.get(&path, Some(2)).await?;
402 debug!(
403 "Historical prices obtained for epic: {}, {} data points",
404 epic,
405 result.prices.len()
406 );
407 Ok(result)
408 }
409
410 async fn get_recent_prices(
411 &self,
412 params: &RecentPricesRequest<'_>,
413 ) -> Result<HistoricalPricesResponse, AppError> {
414 let mut query_params = Vec::new();
415
416 if let Some(res) = params.resolution {
417 query_params.push(format!("resolution={}", res));
418 }
419 if let Some(f) = params.from {
420 query_params.push(format!("from={}", f));
421 }
422 if let Some(t) = params.to {
423 query_params.push(format!("to={}", t));
424 }
425 if let Some(max) = params.max_points {
426 query_params.push(format!("max={}", max));
427 }
428 if let Some(size) = params.page_size {
429 query_params.push(format!("pageSize={}", size));
430 }
431 if let Some(num) = params.page_number {
432 query_params.push(format!("pageNumber={}", num));
433 }
434
435 let query_string = if query_params.is_empty() {
436 String::new()
437 } else {
438 format!("?{}", query_params.join("&"))
439 };
440
441 let path = format!("prices/{}{}", params.epic, query_string);
442 info!("Getting recent prices for epic: {}", params.epic);
443 let result: HistoricalPricesResponse = self.http_client.get(&path, Some(3)).await?;
444 debug!(
445 "Recent prices obtained for epic: {}, {} data points",
446 params.epic,
447 result.prices.len()
448 );
449 Ok(result)
450 }
451
452 async fn get_historical_prices_by_count_v1(
453 &self,
454 epic: &str,
455 resolution: &str,
456 num_points: u32,
457 ) -> Result<HistoricalPricesResponse, AppError> {
458 let path = format!("prices/{}/{}/{}", epic, resolution, num_points);
459 info!(
460 "Getting historical prices (v1) for epic: {}, resolution: {}, points: {}",
461 epic, resolution, num_points
462 );
463 let result: HistoricalPricesResponse = self.http_client.get(&path, Some(1)).await?;
464 debug!(
465 "Historical prices (v1) obtained for epic: {}, {} data points",
466 epic,
467 result.prices.len()
468 );
469 Ok(result)
470 }
471
472 async fn get_historical_prices_by_count_v2(
473 &self,
474 epic: &str,
475 resolution: &str,
476 num_points: u32,
477 ) -> Result<HistoricalPricesResponse, AppError> {
478 let path = format!("prices/{}/{}/{}", epic, resolution, num_points);
479 info!(
480 "Getting historical prices (v2) for epic: {}, resolution: {}, points: {}",
481 epic, resolution, num_points
482 );
483 let result: HistoricalPricesResponse = self.http_client.get(&path, Some(2)).await?;
484 debug!(
485 "Historical prices (v2) obtained for epic: {}, {} data points",
486 epic,
487 result.prices.len()
488 );
489 Ok(result)
490 }
491
492 async fn get_market_navigation(&self) -> Result<MarketNavigationResponse, AppError> {
493 let path = "marketnavigation";
494 info!("Getting top-level market navigation nodes");
495 let result: MarketNavigationResponse = self.http_client.get(path, Some(1)).await?;
496 debug!("{} navigation nodes found", result.nodes.len());
497 debug!("{} markets found at root level", result.markets.len());
498 Ok(result)
499 }
500
501 async fn get_market_navigation_node(
502 &self,
503 node_id: &str,
504 ) -> Result<MarketNavigationResponse, AppError> {
505 let path = format!("marketnavigation/{}", node_id);
506 info!("Getting market navigation node: {}", node_id);
507 let result: MarketNavigationResponse = self.http_client.get(&path, Some(1)).await?;
508 debug!("{} child nodes found", result.nodes.len());
509 debug!("{} markets found in node {}", result.markets.len(), node_id);
510 Ok(result)
511 }
512
513 async fn get_all_markets(&self) -> Result<Vec<MarketData>, AppError> {
514 let max_depth = 6;
515 info!(
516 "Starting comprehensive market hierarchy traversal (max {} levels)",
517 max_depth
518 );
519
520 let root_response = self.get_market_navigation().await?;
521 info!(
522 "Root navigation: {} nodes, {} markets at top level",
523 root_response.nodes.len(),
524 root_response.markets.len()
525 );
526
527 let mut seen_epics: HashSet<String> = HashSet::new();
531 let mut all_markets: Vec<MarketData> = Vec::new();
532 for market in root_response.markets {
533 if seen_epics.insert(market.epic.clone()) {
534 all_markets.push(market);
535 }
536 }
537 let mut nodes_to_process = root_response.nodes;
538 let mut processed_levels = 0;
539
540 while !nodes_to_process.is_empty() && processed_levels < max_depth {
541 let mut next_level_nodes = Vec::new();
542 let mut level_market_count = 0;
543
544 info!(
545 "Processing level {} with {} nodes",
546 processed_levels,
547 nodes_to_process.len()
548 );
549
550 for node in &nodes_to_process {
551 match self.get_market_navigation_node(&node.id).await {
552 Ok(node_response) => {
553 let node_markets = node_response.markets.len();
554 let node_children = node_response.nodes.len();
555
556 if node_markets > 0 || node_children > 0 {
557 debug!(
558 "Node '{}' (level {}): {} markets, {} child nodes",
559 node.name, processed_levels, node_markets, node_children
560 );
561 }
562
563 for market in node_response.markets {
566 if seen_epics.insert(market.epic.clone()) {
567 all_markets.push(market);
568 level_market_count += 1;
569 }
570 }
571 next_level_nodes.extend(node_response.nodes);
572 }
573 Err(e) => {
574 tracing::error!(
575 "Failed to get markets for node '{}' at level {}: {:?}",
576 node.name,
577 processed_levels,
578 e
579 );
580 }
581 }
582 }
583
584 info!(
585 "Level {} completed: {} markets found, {} nodes for next level",
586 processed_levels,
587 level_market_count,
588 next_level_nodes.len()
589 );
590
591 nodes_to_process = next_level_nodes;
592 processed_levels += 1;
593 }
594
595 info!(
596 "Market hierarchy traversal completed: {} total markets found across {} levels",
597 all_markets.len(),
598 processed_levels
599 );
600
601 Ok(all_markets)
602 }
603
604 async fn get_vec_db_entries(&self) -> Result<Vec<DBEntryResponse>, AppError> {
605 info!("Getting all markets from hierarchy for DB entries");
606
607 let all_markets = self.get_all_markets().await?;
608 info!("Collected {} markets from hierarchy", all_markets.len());
609
610 let mut vec_db_entries: Vec<DBEntryResponse> = all_markets
611 .iter()
612 .map(DBEntryResponse::from)
613 .filter(|entry| !entry.epic.is_empty())
614 .collect();
615
616 info!("Created {} DB entries from markets", vec_db_entries.len());
617
618 let mut symbol_info: std::collections::HashMap<String, (String, String)> =
624 std::collections::HashMap::new();
625 for entry in &vec_db_entries {
626 if entry.symbol.is_empty() || entry.epic.is_empty() {
627 continue;
628 }
629 symbol_info
630 .entry(entry.symbol.clone())
631 .or_insert_with(|| (entry.epic.clone(), entry.expiry.clone()));
632 }
633
634 info!(
635 "Found {} unique symbols to fetch expiry dates for",
636 symbol_info.len()
637 );
638
639 let symbol_expiry_map: std::collections::HashMap<String, String> =
643 futures::stream::iter(symbol_info)
644 .map(|(symbol, (epic, fallback_expiry))| async move {
645 match self.get_market_details(&epic).await {
646 Ok(market_details) => {
647 let expiry_date = market_details
648 .instrument
649 .expiry_details
650 .as_ref()
651 .map(|details| details.last_dealing_date.clone())
652 .unwrap_or_else(|| market_details.instrument.expiry.clone());
653
654 info!(
655 symbol = %symbol,
656 expiry = %expiry_date,
657 "fetched expiry date for symbol"
658 );
659 (symbol, expiry_date)
660 }
661 Err(e) => {
662 tracing::error!(
663 "Failed to get market details for epic {} (symbol {}): {:?}",
664 epic,
665 symbol,
666 e
667 );
668 (symbol, fallback_expiry)
669 }
670 }
671 })
672 .buffer_unordered(MARKET_DETAILS_CONCURRENCY)
673 .collect()
674 .await;
675
676 for entry in &mut vec_db_entries {
677 if let Some(expiry_date) = symbol_expiry_map.get(&entry.symbol) {
678 entry.expiry = expiry_date.clone();
679 }
680 }
681
682 info!("Updated expiry dates for {} entries", vec_db_entries.len());
683 Ok(vec_db_entries)
684 }
685
686 async fn get_categories(&self) -> Result<CategoriesResponse, AppError> {
687 info!("Getting all categories of instruments");
688 let result: CategoriesResponse = self.http_client.get("categories", Some(1)).await?;
689 debug!("{} categories found", result.categories.len());
690 Ok(result)
691 }
692
693 async fn get_category_instruments(
694 &self,
695 category_id: &str,
696 page_number: Option<u32>,
697 page_size: Option<u32>,
698 ) -> Result<CategoryInstrumentsResponse, AppError> {
699 let mut path = format!("categories/{}/instruments", category_id);
700
701 let mut query_params = Vec::new();
702 if let Some(page) = page_number {
703 query_params.push(format!("pageNumber={}", page));
704 }
705 if let Some(size) = page_size {
706 if size > 1000 {
707 return Err(AppError::InvalidInput(
708 "pageSize cannot exceed 1000".to_string(),
709 ));
710 }
711 query_params.push(format!("pageSize={}", size));
712 }
713
714 if !query_params.is_empty() {
715 path = format!("{}?{}", path, query_params.join("&"));
716 }
717
718 info!(
719 "Getting instruments for category: {} (page: {:?}, size: {:?})",
720 category_id, page_number, page_size
721 );
722 let result: CategoryInstrumentsResponse = self.http_client.get(&path, Some(1)).await?;
723 debug!(
724 "{} instruments found in category {}",
725 result.instruments.len(),
726 category_id
727 );
728 Ok(result)
729 }
730}
731
732#[async_trait]
733impl AccountService for Client {
734 async fn get_accounts(&self) -> Result<AccountsResponse, AppError> {
735 info!("Getting account information");
736 let result: AccountsResponse = self.http_client.get("accounts", Some(1)).await?;
737 debug!(
738 "Account information obtained: {} accounts",
739 result.accounts.len()
740 );
741 Ok(result)
742 }
743
744 async fn get_positions(&self) -> Result<PositionsResponse, AppError> {
745 debug!("Getting open positions");
746 let result: PositionsResponse = self.http_client.get("positions", Some(2)).await?;
747 debug!("Positions obtained: {} positions", result.positions.len());
748 Ok(result)
749 }
750
751 async fn get_positions_w_filter(&self, filter: &str) -> Result<PositionsResponse, AppError> {
752 debug!("Getting open positions with filter: {}", filter);
753 let mut positions = self.get_positions().await?;
754
755 positions
756 .positions
757 .retain(|position| position.market.epic.contains(filter));
758
759 debug!(
760 "Positions obtained after filtering: {} positions",
761 positions.positions.len()
762 );
763 Ok(positions)
764 }
765
766 async fn get_working_orders(&self) -> Result<WorkingOrdersResponse, AppError> {
767 info!("Getting working orders");
768 let result: WorkingOrdersResponse = self.http_client.get("workingorders", Some(2)).await?;
769 debug!(
770 "Working orders obtained: {} orders",
771 result.working_orders.len()
772 );
773 Ok(result)
774 }
775
776 async fn get_activity(
777 &self,
778 from: &str,
779 to: &str,
780 ) -> Result<AccountActivityResponse, AppError> {
781 let path = format!("history/activity?from={}&to={}&pageSize=500", from, to);
782 info!("Getting account activity");
783 let result: AccountActivityResponse = self.http_client.get(&path, Some(3)).await?;
784 debug!(
785 "Account activity obtained: {} activities",
786 result.activities.len()
787 );
788 Ok(result)
789 }
790
791 async fn get_activity_with_details(
792 &self,
793 from: &str,
794 to: &str,
795 ) -> Result<AccountActivityResponse, AppError> {
796 let path = format!(
797 "history/activity?from={}&to={}&detailed=true&pageSize=500",
798 from, to
799 );
800 info!("Getting detailed account activity");
801 let result: AccountActivityResponse = self.http_client.get(&path, Some(3)).await?;
802 debug!(
803 "Detailed account activity obtained: {} activities",
804 result.activities.len()
805 );
806 Ok(result)
807 }
808
809 async fn get_transactions(
810 &self,
811 from: &str,
812 to: &str,
813 ) -> Result<TransactionHistoryResponse, AppError> {
814 const PAGE_SIZE: u32 = 200;
815 let mut all_transactions = Vec::new();
816 let mut current_page = 1;
817 #[allow(unused_assignments)]
818 let mut last_metadata = None;
819
820 loop {
821 let path = format!(
822 "history/transactions?from={}&to={}&pageSize={}&pageNumber={}",
823 from, to, PAGE_SIZE, current_page
824 );
825 info!("Getting transaction history page {}", current_page);
826
827 let result: TransactionHistoryResponse = self.http_client.get(&path, Some(2)).await?;
828
829 let total_pages = result.metadata.page_data.total_pages as u32;
830 last_metadata = Some(result.metadata);
831 all_transactions.extend(result.transactions);
832
833 if current_page >= total_pages {
834 break;
835 }
836 current_page += 1;
837 }
838
839 debug!(
840 "Total transaction history obtained: {} transactions",
841 all_transactions.len()
842 );
843
844 Ok(TransactionHistoryResponse {
845 transactions: all_transactions,
846 metadata: last_metadata
847 .ok_or_else(|| AppError::InvalidInput("Could not retrieve metadata".to_string()))?,
848 })
849 }
850
851 async fn get_preferences(&self) -> Result<AccountPreferencesResponse, AppError> {
852 info!("Getting account preferences");
853 let result: AccountPreferencesResponse = self
854 .http_client
855 .get("accounts/preferences", Some(1))
856 .await?;
857 debug!(
858 "Account preferences obtained: trailing_stops_enabled={}",
859 result.trailing_stops_enabled
860 );
861 Ok(result)
862 }
863
864 async fn update_preferences(&self, trailing_stops_enabled: bool) -> Result<(), AppError> {
865 info!(
866 "Updating account preferences: trailing_stops_enabled={}",
867 trailing_stops_enabled
868 );
869 let request = serde_json::json!({
870 "trailingStopsEnabled": trailing_stops_enabled
871 });
872 let _: serde_json::Value = self
873 .http_client
874 .put("accounts/preferences", &request, Some(1))
875 .await?;
876 debug!("Account preferences updated");
877 Ok(())
878 }
879
880 async fn get_activity_by_period(
881 &self,
882 period_ms: u64,
883 ) -> Result<AccountActivityResponse, AppError> {
884 let path = format!("history/activity/{}", period_ms);
885 info!("Getting account activity for period: {} ms", period_ms);
886 let result: AccountActivityResponse = self.http_client.get(&path, Some(1)).await?;
887 debug!(
888 "Account activity obtained: {} activities",
889 result.activities.len()
890 );
891 Ok(result)
892 }
893}
894
895#[async_trait]
896impl OrderService for Client {
897 async fn create_order(
898 &self,
899 order: &CreateOrderRequest,
900 ) -> Result<CreateOrderResponse, AppError> {
901 info!("Creating order for: {}", order.epic);
902 let result: CreateOrderResponse = self
903 .http_client
904 .post("positions/otc", order, Some(2))
905 .await?;
906 debug!("Order created with reference: {}", result.deal_reference);
907 Ok(result)
908 }
909
910 async fn get_order_confirmation(
911 &self,
912 deal_reference: &str,
913 ) -> Result<OrderConfirmationResponse, AppError> {
914 let path = format!("confirms/{}", deal_reference);
915 info!("Getting confirmation for order: {}", deal_reference);
916 let result: OrderConfirmationResponse = self.http_client.get(&path, Some(1)).await?;
917 debug!("Confirmation obtained for order: {}", deal_reference);
918 Ok(result)
919 }
920
921 async fn get_order_confirmation_w_retry(
922 &self,
923 deal_reference: &str,
924 retries: u64,
925 delay_ms: u64,
926 ) -> Result<OrderConfirmationResponse, AppError> {
927 let base = Duration::from_millis(delay_ms);
930 let mut attempt: u32 = 0;
931 loop {
932 match self.get_order_confirmation(deal_reference).await {
933 Ok(response) => return Ok(response),
934 Err(e) => {
935 if !is_transient_confirmation_error(&e) {
938 return Err(e);
939 }
940 if u64::from(attempt) >= retries {
941 return Err(e);
942 }
943 let delay = backoff_delay(base, attempt);
944 let delay_ms = u64::try_from(delay.as_millis()).unwrap_or(u64::MAX);
945 let next_attempt = attempt.checked_add(1).ok_or_else(|| {
946 AppError::Generic("retry attempt counter overflow".to_string())
947 })?;
948 warn!(
949 deal_reference = %deal_reference,
950 attempt = next_attempt,
951 max_retries = retries,
952 delay_ms,
953 "retrying order confirmation after transient error"
954 );
955 sleep(delay).await;
956 attempt = next_attempt;
957 }
958 }
959 }
960 }
961
962 async fn update_position(
963 &self,
964 deal_id: &str,
965 update: &UpdatePositionRequest,
966 ) -> Result<UpdatePositionResponse, AppError> {
967 let path = format!("positions/otc/{}", deal_id);
968 info!("Updating position: {}", deal_id);
969 let result: UpdatePositionResponse = self.http_client.put(&path, update, Some(2)).await?;
970 debug!(
971 "Position updated: {} with deal reference: {}",
972 deal_id, result.deal_reference
973 );
974 Ok(result)
975 }
976
977 async fn update_level_in_position(
978 &self,
979 deal_id: &str,
980 limit_level: Option<f64>,
981 ) -> Result<UpdatePositionResponse, AppError> {
982 let path = format!("positions/otc/{}", deal_id);
983 info!("Updating position: {}", deal_id);
984 let limit_level = limit_level.unwrap_or(0.0);
985
986 let update: UpdatePositionRequest = UpdatePositionRequest {
987 guaranteed_stop: None,
988 limit_level: Some(limit_level),
989 stop_level: None,
990 trailing_stop: None,
991 trailing_stop_distance: None,
992 trailing_stop_increment: None,
993 };
994 let result: UpdatePositionResponse = self.http_client.put(&path, update, Some(2)).await?;
995 debug!(
996 "Position updated: {} with deal reference: {}",
997 deal_id, result.deal_reference
998 );
999 Ok(result)
1000 }
1001
1002 async fn close_position(
1003 &self,
1004 close_request: &ClosePositionRequest,
1005 ) -> Result<ClosePositionResponse, AppError> {
1006 info!("Closing position");
1007
1008 let result: ClosePositionResponse = self
1011 .http_client
1012 .post_with_delete_method("positions/otc", close_request, Some(1))
1013 .await?;
1014
1015 debug!("Position closed with reference: {}", result.deal_reference);
1016 Ok(result)
1017 }
1018
1019 async fn create_working_order(
1020 &self,
1021 order: &CreateWorkingOrderRequest,
1022 ) -> Result<CreateWorkingOrderResponse, AppError> {
1023 info!("Creating working order for: {}", order.epic);
1024 let result: CreateWorkingOrderResponse = self
1025 .http_client
1026 .post("workingorders/otc", order, Some(2))
1027 .await?;
1028 debug!(
1029 "Working order created with reference: {}",
1030 result.deal_reference
1031 );
1032 Ok(result)
1033 }
1034
1035 async fn delete_working_order(&self, deal_id: &str) -> Result<(), AppError> {
1036 let path = format!("workingorders/otc/{}", deal_id);
1037 let result: CreateWorkingOrderResponse =
1038 self.http_client.delete(path.as_str(), Some(2)).await?;
1039 debug!(
1040 "Working order created with reference: {}",
1041 result.deal_reference
1042 );
1043 Ok(())
1044 }
1045
1046 async fn get_position(&self, deal_id: &str) -> Result<SinglePositionResponse, AppError> {
1047 let path = format!("positions/{}", deal_id);
1048 info!("Getting position: {}", deal_id);
1049 let result: SinglePositionResponse = self.http_client.get(&path, Some(2)).await?;
1050 debug!("Position obtained for deal: {}", deal_id);
1051 Ok(result)
1052 }
1053
1054 async fn update_working_order(
1055 &self,
1056 deal_id: &str,
1057 update: &UpdateWorkingOrderRequest,
1058 ) -> Result<CreateWorkingOrderResponse, AppError> {
1059 let path = format!("workingorders/otc/{}", deal_id);
1060 info!("Updating working order: {}", deal_id);
1061 let result: CreateWorkingOrderResponse =
1062 self.http_client.put(&path, update, Some(2)).await?;
1063 debug!(
1064 "Working order updated: {} with reference: {}",
1065 deal_id, result.deal_reference
1066 );
1067 Ok(result)
1068 }
1069}
1070
1071#[async_trait]
1076impl WatchlistService for Client {
1077 async fn get_watchlists(&self) -> Result<WatchlistsResponse, AppError> {
1078 info!("Getting all watchlists");
1079 let result: WatchlistsResponse = self.http_client.get("watchlists", Some(1)).await?;
1080 debug!(
1081 "Watchlists obtained: {} watchlists",
1082 result.watchlists.len()
1083 );
1084 Ok(result)
1085 }
1086
1087 async fn create_watchlist(
1088 &self,
1089 name: &str,
1090 epics: Option<&[String]>,
1091 ) -> Result<CreateWatchlistResponse, AppError> {
1092 info!("Creating watchlist: {}", name);
1093 let request = CreateWatchlistRequest {
1094 name: name.to_string(),
1095 epics: epics.map(|e| e.to_vec()),
1096 };
1097 let result: CreateWatchlistResponse = self
1098 .http_client
1099 .post("watchlists", &request, Some(1))
1100 .await?;
1101 debug!(
1102 "Watchlist created: {} with ID: {}",
1103 name, result.watchlist_id
1104 );
1105 Ok(result)
1106 }
1107
1108 async fn get_watchlist(
1109 &self,
1110 watchlist_id: &str,
1111 ) -> Result<WatchlistMarketsResponse, AppError> {
1112 let path = format!("watchlists/{}", watchlist_id);
1113 info!("Getting watchlist: {}", watchlist_id);
1114 let result: WatchlistMarketsResponse = self.http_client.get(&path, Some(1)).await?;
1115 debug!(
1116 "Watchlist obtained: {} with {} markets",
1117 watchlist_id,
1118 result.markets.len()
1119 );
1120 Ok(result)
1121 }
1122
1123 async fn delete_watchlist(&self, watchlist_id: &str) -> Result<StatusResponse, AppError> {
1124 let path = format!("watchlists/{}", watchlist_id);
1125 info!("Deleting watchlist: {}", watchlist_id);
1126 let result: StatusResponse = self.http_client.delete(&path, Some(1)).await?;
1127 debug!("Watchlist deleted: {}", watchlist_id);
1128 Ok(result)
1129 }
1130
1131 async fn add_to_watchlist(
1132 &self,
1133 watchlist_id: &str,
1134 epic: &str,
1135 ) -> Result<StatusResponse, AppError> {
1136 let path = format!("watchlists/{}", watchlist_id);
1137 info!("Adding {} to watchlist: {}", epic, watchlist_id);
1138 let request = AddToWatchlistRequest {
1139 epic: epic.to_string(),
1140 };
1141 let result: StatusResponse = self.http_client.put(&path, &request, Some(1)).await?;
1142 debug!("Added {} to watchlist: {}", epic, watchlist_id);
1143 Ok(result)
1144 }
1145
1146 async fn remove_from_watchlist(
1147 &self,
1148 watchlist_id: &str,
1149 epic: &str,
1150 ) -> Result<StatusResponse, AppError> {
1151 let path = format!("watchlists/{}/{}", watchlist_id, epic);
1152 info!("Removing {} from watchlist: {}", epic, watchlist_id);
1153 let result: StatusResponse = self.http_client.delete(&path, Some(1)).await?;
1154 debug!("Removed {} from watchlist: {}", epic, watchlist_id);
1155 Ok(result)
1156 }
1157}
1158
1159#[async_trait]
1164impl SentimentService for Client {
1165 async fn get_client_sentiment(
1166 &self,
1167 market_ids: &[String],
1168 ) -> Result<ClientSentimentResponse, AppError> {
1169 let market_ids_str = market_ids.join(",");
1170 let path = format!("clientsentiment?marketIds={}", market_ids_str);
1171 info!("Getting client sentiment for {} markets", market_ids.len());
1172 let result: ClientSentimentResponse = self.http_client.get(&path, Some(1)).await?;
1173 debug!(
1174 "Client sentiment obtained for {} markets",
1175 result.client_sentiments.len()
1176 );
1177 Ok(result)
1178 }
1179
1180 async fn get_client_sentiment_by_market(
1181 &self,
1182 market_id: &str,
1183 ) -> Result<MarketSentiment, AppError> {
1184 let path = format!("clientsentiment/{}", market_id);
1185 info!("Getting client sentiment for market: {}", market_id);
1186 let result: MarketSentiment = self.http_client.get(&path, Some(1)).await?;
1187 debug!(
1188 "Client sentiment for {}: {}% long, {}% short",
1189 market_id, result.long_position_percentage, result.short_position_percentage
1190 );
1191 Ok(result)
1192 }
1193
1194 async fn get_related_sentiment(
1195 &self,
1196 market_id: &str,
1197 ) -> Result<ClientSentimentResponse, AppError> {
1198 let path = format!("clientsentiment/related/{}", market_id);
1199 info!("Getting related sentiment for market: {}", market_id);
1200 let result: ClientSentimentResponse = self.http_client.get(&path, Some(1)).await?;
1201 debug!(
1202 "Related sentiment obtained: {} markets",
1203 result.client_sentiments.len()
1204 );
1205 Ok(result)
1206 }
1207}
1208
1209#[async_trait]
1214impl CostsService for Client {
1215 async fn get_indicative_costs_open(
1216 &self,
1217 request: &OpenCostsRequest,
1218 ) -> Result<IndicativeCostsResponse, AppError> {
1219 info!(
1220 "Getting indicative costs for opening position on: {}",
1221 request.epic
1222 );
1223 let result: IndicativeCostsResponse = self
1224 .http_client
1225 .post("indicativecostsandcharges/open", request, Some(1))
1226 .await?;
1227 debug!(
1228 "Indicative costs obtained, reference: {}",
1229 result.indicative_quote_reference
1230 );
1231 Ok(result)
1232 }
1233
1234 async fn get_indicative_costs_close(
1235 &self,
1236 request: &CloseCostsRequest,
1237 ) -> Result<IndicativeCostsResponse, AppError> {
1238 info!(
1239 "Getting indicative costs for closing position: {}",
1240 request.deal_id
1241 );
1242 let result: IndicativeCostsResponse = self
1243 .http_client
1244 .post("indicativecostsandcharges/close", request, Some(1))
1245 .await?;
1246 debug!(
1247 "Indicative costs obtained, reference: {}",
1248 result.indicative_quote_reference
1249 );
1250 Ok(result)
1251 }
1252
1253 async fn get_indicative_costs_edit(
1254 &self,
1255 request: &EditCostsRequest,
1256 ) -> Result<IndicativeCostsResponse, AppError> {
1257 info!(
1258 "Getting indicative costs for editing position: {}",
1259 request.deal_id
1260 );
1261 let result: IndicativeCostsResponse = self
1262 .http_client
1263 .post("indicativecostsandcharges/edit", request, Some(1))
1264 .await?;
1265 debug!(
1266 "Indicative costs obtained, reference: {}",
1267 result.indicative_quote_reference
1268 );
1269 Ok(result)
1270 }
1271
1272 async fn get_costs_history(
1273 &self,
1274 from: &str,
1275 to: &str,
1276 ) -> Result<CostsHistoryResponse, AppError> {
1277 let path = format!("indicativecostsandcharges/history/from/{}/to/{}", from, to);
1278 info!("Getting costs history from {} to {}", from, to);
1279 let result: CostsHistoryResponse = self.http_client.get(&path, Some(1)).await?;
1280 debug!("Costs history obtained: {} entries", result.costs.len());
1281 Ok(result)
1282 }
1283
1284 async fn get_durable_medium(
1285 &self,
1286 quote_reference: &str,
1287 ) -> Result<DurableMediumResponse, AppError> {
1288 let path = format!(
1289 "indicativecostsandcharges/durablemedium/{}",
1290 quote_reference
1291 );
1292 info!("Getting durable medium for reference: {}", quote_reference);
1293 let result: DurableMediumResponse = self.http_client.get(&path, Some(1)).await?;
1294 debug!("Durable medium obtained for reference: {}", quote_reference);
1295 Ok(result)
1296 }
1297}
1298
1299#[async_trait]
1304impl OperationsService for Client {
1305 async fn get_client_apps(&self) -> Result<ApplicationDetailsResponse, AppError> {
1306 info!("Getting client applications");
1307 let result: ApplicationDetailsResponse = self
1308 .http_client
1309 .get("operations/application", Some(1))
1310 .await?;
1311 debug!(
1313 name = ?result.name,
1314 status = %result.status,
1315 "Client application obtained"
1316 );
1317 Ok(result)
1318 }
1319
1320 async fn disable_client_app(&self) -> Result<StatusResponse, AppError> {
1321 info!("Disabling current client application");
1322 let result: StatusResponse = self
1323 .http_client
1324 .put(
1325 "operations/application/disable",
1326 &serde_json::json!({}),
1327 Some(1),
1328 )
1329 .await?;
1330 debug!("Client application disabled");
1331 Ok(result)
1332 }
1333}
1334
1335#[cfg(feature = "streaming")]
1345#[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
1346pub struct StreamerClient {
1347 account_id: String,
1348 market_streamer_client: Option<Arc<Mutex<LightstreamerClient>>>,
1349 price_streamer_client: Option<Arc<Mutex<LightstreamerClient>>>,
1350 has_market_stream_subs: bool,
1352 has_price_stream_subs: bool,
1353 converter_tasks: Vec<JoinHandle<()>>,
1359}
1360
1361#[cfg(feature = "streaming")]
1362impl StreamerClient {
1363 pub async fn new() -> Result<Self, AppError> {
1378 let client = Client::try_new()?;
1379 Self::with_client(&client).await
1380 }
1381
1382 pub async fn with_client(client: &Client) -> Result<Self, AppError> {
1396 let ws_info = client.ws_info().await?;
1397 let password = ws_info.get_ws_password();
1398
1399 let market_streamer_client = Arc::new(Mutex::new(LightstreamerClient::new(
1401 Some(ws_info.server.as_str()),
1402 None,
1403 Some(&ws_info.account_id),
1404 Some(&password),
1405 )?));
1406
1407 let price_streamer_client = Arc::new(Mutex::new(LightstreamerClient::new(
1408 Some(ws_info.server.as_str()),
1409 None,
1410 Some(&ws_info.account_id),
1411 Some(&password),
1412 )?));
1413
1414 {
1417 let mut streamer = market_streamer_client.lock().await;
1418 streamer
1419 .connection_options
1420 .set_forced_transport(Some(Transport::WsStreaming));
1421 streamer.set_logging_type(LogType::TracingLogs);
1422 }
1423 {
1424 let mut streamer = price_streamer_client.lock().await;
1425 streamer
1426 .connection_options
1427 .set_forced_transport(Some(Transport::WsStreaming));
1428 streamer.set_logging_type(LogType::TracingLogs);
1429 }
1430
1431 Ok(Self {
1432 account_id: ws_info.account_id.clone(),
1433 market_streamer_client: Some(market_streamer_client),
1434 price_streamer_client: Some(price_streamer_client),
1435 has_market_stream_subs: false,
1436 has_price_stream_subs: false,
1437 converter_tasks: Vec::new(),
1438 })
1439 }
1440
1441 pub async fn market_subscribe(
1471 &mut self,
1472 epics: Vec<String>,
1473 fields: HashSet<StreamingMarketField>,
1474 ) -> Result<mpsc::UnboundedReceiver<PriceData>, AppError> {
1475 self.has_market_stream_subs = true;
1477
1478 let fields = get_streaming_market_fields(&fields);
1479 let market_epics: Vec<String> = epics
1480 .iter()
1481 .map(|epic| "MARKET:".to_string() + epic)
1482 .collect();
1483 let mut subscription =
1484 Subscription::new(SubscriptionMode::Merge, Some(market_epics), Some(fields))?;
1485
1486 subscription.set_data_adapter(None)?;
1487 subscription.set_requested_snapshot(Some(Snapshot::Yes))?;
1488
1489 let (listener, item_receiver) = ChannelSubscriptionListener::create_channel();
1491 subscription.add_listener(Box::new(listener));
1492
1493 let client = self.market_streamer_client.as_ref().ok_or_else(|| {
1495 AppError::WebSocketError("market streamer client not initialized".to_string())
1496 })?;
1497
1498 {
1499 let mut client = client.lock().await;
1500 client
1501 .connection_options
1502 .set_forced_transport(Some(Transport::WsStreaming));
1503 LightstreamerClient::subscribe(client.subscription_sender.clone(), subscription)
1504 .await?;
1505 }
1506
1507 let (price_tx, price_rx) = mpsc::unbounded_channel();
1509 let handle = tokio::spawn(async move {
1510 let mut receiver = item_receiver;
1511 while let Some(item_update) = receiver.recv().await {
1512 let price_data = PriceData::from(&item_update);
1513 if price_tx.send(price_data).is_err() {
1514 tracing::debug!("Price channel receiver dropped");
1515 break;
1516 }
1517 }
1518 });
1519 self.converter_tasks.push(handle);
1521
1522 info!(
1523 "Market subscription created for {} instruments",
1524 epics.len()
1525 );
1526 Ok(price_rx)
1527 }
1528
1529 pub async fn trade_subscribe(
1552 &mut self,
1553 ) -> Result<mpsc::UnboundedReceiver<TradeFields>, AppError> {
1554 self.has_market_stream_subs = true;
1556
1557 let account_id = self.account_id.clone();
1558 let fields = Some(vec![
1559 "CONFIRMS".to_string(),
1560 "OPU".to_string(),
1561 "WOU".to_string(),
1562 ]);
1563 let trade_items = vec![format!("TRADE:{account_id}")];
1564
1565 let mut subscription =
1566 Subscription::new(SubscriptionMode::Distinct, Some(trade_items), fields)?;
1567
1568 subscription.set_data_adapter(None)?;
1569 subscription.set_requested_snapshot(Some(Snapshot::Yes))?;
1570
1571 let (listener, item_receiver) = ChannelSubscriptionListener::create_channel();
1573 subscription.add_listener(Box::new(listener));
1574
1575 let client = self.market_streamer_client.as_ref().ok_or_else(|| {
1577 AppError::WebSocketError("market streamer client not initialized".to_string())
1578 })?;
1579
1580 {
1581 let mut client = client.lock().await;
1582 client
1583 .connection_options
1584 .set_forced_transport(Some(Transport::WsStreaming));
1585 LightstreamerClient::subscribe(client.subscription_sender.clone(), subscription)
1586 .await?;
1587 }
1588
1589 let (trade_tx, trade_rx) = mpsc::unbounded_channel();
1591 let handle = tokio::spawn(async move {
1592 let mut receiver = item_receiver;
1593 while let Some(item_update) = receiver.recv().await {
1594 let trade_data = crate::presentation::trade::TradeData::from(&item_update);
1595 if trade_tx.send(trade_data.fields).is_err() {
1596 tracing::debug!("Trade channel receiver dropped");
1597 break;
1598 }
1599 }
1600 });
1601 self.converter_tasks.push(handle);
1603
1604 info!("Trade subscription created for account: {}", account_id);
1605 Ok(trade_rx)
1606 }
1607
1608 pub async fn account_subscribe(
1635 &mut self,
1636 fields: HashSet<StreamingAccountDataField>,
1637 ) -> Result<mpsc::UnboundedReceiver<AccountFields>, AppError> {
1638 self.has_market_stream_subs = true;
1640
1641 let fields = get_streaming_account_data_fields(&fields);
1642 let account_id = self.account_id.clone();
1643 let account_items = vec![format!("ACCOUNT:{account_id}")];
1644
1645 let mut subscription =
1646 Subscription::new(SubscriptionMode::Merge, Some(account_items), Some(fields))?;
1647
1648 subscription.set_data_adapter(None)?;
1649 subscription.set_requested_snapshot(Some(Snapshot::Yes))?;
1650
1651 let (listener, item_receiver) = ChannelSubscriptionListener::create_channel();
1653 subscription.add_listener(Box::new(listener));
1654
1655 let client = self.market_streamer_client.as_ref().ok_or_else(|| {
1657 AppError::WebSocketError("market streamer client not initialized".to_string())
1658 })?;
1659
1660 {
1661 let mut client = client.lock().await;
1662 client
1663 .connection_options
1664 .set_forced_transport(Some(Transport::WsStreaming));
1665 LightstreamerClient::subscribe(client.subscription_sender.clone(), subscription)
1666 .await?;
1667 }
1668
1669 let (account_tx, account_rx) = mpsc::unbounded_channel();
1671 let handle = tokio::spawn(async move {
1672 let mut receiver = item_receiver;
1673 while let Some(item_update) = receiver.recv().await {
1674 let account_data = crate::presentation::account::AccountData::from(&item_update);
1675 if account_tx.send(account_data.fields).is_err() {
1676 tracing::debug!("Account channel receiver dropped");
1677 break;
1678 }
1679 }
1680 });
1681 self.converter_tasks.push(handle);
1683
1684 info!("Account subscription created for account: {}", account_id);
1685 Ok(account_rx)
1686 }
1687
1688 pub async fn price_subscribe(
1719 &mut self,
1720 epics: Vec<String>,
1721 fields: HashSet<StreamingPriceField>,
1722 ) -> Result<mpsc::UnboundedReceiver<PriceData>, AppError> {
1723 self.has_price_stream_subs = true;
1725
1726 let fields = get_streaming_price_fields(&fields);
1727 let account_id = self.account_id.clone();
1728 let price_epics: Vec<String> = epics
1729 .iter()
1730 .map(|epic| format!("PRICE:{account_id}:{epic}"))
1731 .collect();
1732
1733 tracing::debug!("Pricing subscribe items: {:?}", price_epics);
1735 tracing::debug!("Pricing subscribe fields: {:?}", fields);
1736
1737 let mut subscription =
1738 Subscription::new(SubscriptionMode::Merge, Some(price_epics), Some(fields))?;
1739
1740 let pricing_adapter =
1742 std::env::var("IG_PRICING_ADAPTER").unwrap_or_else(|_| "Pricing".to_string());
1743 tracing::debug!("Using Pricing data adapter: {}", pricing_adapter);
1744 subscription.set_data_adapter(Some(pricing_adapter))?;
1745 subscription.set_requested_snapshot(Some(Snapshot::Yes))?;
1746
1747 let (listener, item_receiver) = ChannelSubscriptionListener::create_channel();
1749 subscription.add_listener(Box::new(listener));
1750
1751 let client = self.price_streamer_client.as_ref().ok_or_else(|| {
1753 AppError::WebSocketError("price streamer client not initialized".to_string())
1754 })?;
1755
1756 {
1757 let mut client = client.lock().await;
1758 client
1759 .connection_options
1760 .set_forced_transport(Some(Transport::WsStreaming));
1761 LightstreamerClient::subscribe(client.subscription_sender.clone(), subscription)
1762 .await?;
1763 }
1764
1765 let (price_tx, price_rx) = mpsc::unbounded_channel();
1767 let handle = tokio::spawn(async move {
1768 let mut receiver = item_receiver;
1769 while let Some(item_update) = receiver.recv().await {
1770 let price_data = PriceData::from(&item_update);
1771 if price_tx.send(price_data).is_err() {
1772 tracing::debug!("Price channel receiver dropped");
1773 break;
1774 }
1775 }
1776 });
1777 self.converter_tasks.push(handle);
1779
1780 info!(
1781 "Price subscription created for {} instruments (account: {})",
1782 epics.len(),
1783 account_id
1784 );
1785 Ok(price_rx)
1786 }
1787
1788 pub async fn chart_subscribe(
1821 &mut self,
1822 epics: Vec<String>,
1823 scale: ChartScale,
1824 fields: HashSet<StreamingChartField>,
1825 ) -> Result<mpsc::UnboundedReceiver<ChartData>, AppError> {
1826 self.has_market_stream_subs = true;
1828
1829 let fields = get_streaming_chart_fields(&fields);
1830
1831 let chart_items: Vec<String> = epics
1832 .iter()
1833 .map(|epic| format!("CHART:{epic}:{scale}",))
1834 .collect();
1835
1836 let mode = if matches!(scale, ChartScale::Tick) {
1838 SubscriptionMode::Distinct
1839 } else {
1840 SubscriptionMode::Merge
1841 };
1842
1843 let mut subscription = Subscription::new(mode, Some(chart_items), Some(fields))?;
1844
1845 subscription.set_data_adapter(None)?;
1846 subscription.set_requested_snapshot(Some(Snapshot::Yes))?;
1847
1848 let (listener, item_receiver) = ChannelSubscriptionListener::create_channel();
1850 subscription.add_listener(Box::new(listener));
1851
1852 let client = self.market_streamer_client.as_ref().ok_or_else(|| {
1854 AppError::WebSocketError("market streamer client not initialized".to_string())
1855 })?;
1856
1857 {
1858 let mut client = client.lock().await;
1859 client
1860 .connection_options
1861 .set_forced_transport(Some(Transport::WsStreaming));
1862 LightstreamerClient::subscribe(client.subscription_sender.clone(), subscription)
1863 .await?;
1864 }
1865
1866 let (chart_tx, chart_rx) = mpsc::unbounded_channel();
1868 let handle = tokio::spawn(async move {
1869 let mut receiver = item_receiver;
1870 while let Some(item_update) = receiver.recv().await {
1871 let chart_data = ChartData::from(&item_update);
1872 if chart_tx.send(chart_data).is_err() {
1873 tracing::debug!("Chart channel receiver dropped");
1874 break;
1875 }
1876 }
1877 });
1878 self.converter_tasks.push(handle);
1880
1881 info!(
1882 "Chart subscription created for {} instruments (scale: {})",
1883 epics.len(),
1884 scale
1885 );
1886
1887 Ok(chart_rx)
1888 }
1889
1890 pub async fn connect(&mut self, shutdown_signal: Option<Arc<Notify>>) -> Result<(), AppError> {
1907 let signal = if let Some(sig) = shutdown_signal {
1909 sig
1910 } else {
1911 let sig = Arc::new(Notify::new());
1912 setup_signal_hook(Arc::clone(&sig)).await;
1913 sig
1914 };
1915
1916 let mut tasks = Vec::new();
1917 let mut connection_signals: Vec<Arc<Notify>> = Vec::new();
1922
1923 if self.has_market_stream_subs {
1925 if let Some(client) = self.market_streamer_client.as_ref() {
1926 let client = Arc::clone(client);
1927 let conn_signal = Arc::new(Notify::new());
1928 connection_signals.push(Arc::clone(&conn_signal));
1929 let task = tokio::spawn(async move {
1930 Self::connect_client(client, conn_signal, "Market").await
1931 });
1932 tasks.push(task);
1933 }
1934 } else {
1935 info!("Skipping Market streamer connection: no active subscriptions");
1936 }
1937
1938 if self.has_price_stream_subs {
1940 if let Some(client) = self.price_streamer_client.as_ref() {
1941 let client = Arc::clone(client);
1942 let conn_signal = Arc::new(Notify::new());
1943 connection_signals.push(Arc::clone(&conn_signal));
1944 let task = tokio::spawn(async move {
1945 Self::connect_client(client, conn_signal, "Price").await
1946 });
1947 tasks.push(task);
1948 }
1949 } else {
1950 info!("Skipping Price streamer connection: no active subscriptions");
1951 }
1952
1953 if tasks.is_empty() {
1954 warn!("No streaming clients selected for connection (no active subscriptions)");
1955 return Ok(());
1956 }
1957
1958 info!("Connecting {} streaming client(s)...", tasks.len());
1959
1960 let fanout = spawn_shutdown_fanout(Arc::clone(&signal), connection_signals);
1964
1965 let results = futures::future::join_all(tasks).await;
1967
1968 fanout.abort();
1971
1972 let mut has_error = false;
1974 for (idx, result) in results.iter().enumerate() {
1975 match result {
1976 Ok(Ok(_)) => {
1977 debug!("Streaming client {} completed successfully", idx);
1978 }
1979 Ok(Err(e)) => {
1980 error!("Streaming client {} failed: {:?}", idx, e);
1981 has_error = true;
1982 }
1983 Err(e) => {
1984 error!("Streaming client {} task panicked: {:?}", idx, e);
1985 has_error = true;
1986 }
1987 }
1988 }
1989
1990 if has_error {
1991 return Err(AppError::WebSocketError(
1992 "one or more streaming connections failed".to_string(),
1993 ));
1994 }
1995
1996 info!("All streaming connections closed gracefully");
1997 Ok(())
1998 }
1999
2000 async fn connect_client(
2002 client: Arc<Mutex<LightstreamerClient>>,
2003 signal: Arc<Notify>,
2004 client_type: &str,
2005 ) -> Result<(), AppError> {
2006 let mut retry_interval_millis: u64 = 0;
2007 let mut retry_counter: u64 = 0;
2008
2009 while retry_counter < MAX_CONNECTION_ATTEMPTS {
2010 let connect_result = {
2011 let mut client = client.lock().await;
2012 client.connect_direct(Arc::clone(&signal)).await
2013 };
2014
2015 match connect_result {
2016 Ok(()) => {
2017 info!("{} streamer connected successfully", client_type);
2018 break;
2019 }
2020 Err(e) => {
2021 if is_graceful_close(&e) {
2026 info!(
2027 "{} streamer closed gracefully: no active subscriptions (server reason: {})",
2028 client_type, GRACEFUL_CLOSE_MARKER
2029 );
2030 return Ok(());
2031 }
2032
2033 let error_msg = e.to_string();
2037 error!("{} streamer connection failed: {}", client_type, error_msg);
2038
2039 if retry_counter < MAX_CONNECTION_ATTEMPTS - 1 {
2040 sleep(Duration::from_millis(retry_interval_millis)).await;
2041 retry_interval_millis =
2042 (retry_interval_millis + (200 * retry_counter)).min(5000);
2043 retry_counter += 1;
2044 warn!(
2045 "{} streamer retrying (attempt {}/{}) in {:.2} seconds...",
2046 client_type,
2047 retry_counter + 1,
2048 MAX_CONNECTION_ATTEMPTS,
2049 retry_interval_millis as f64 / 1000.0
2050 );
2051 } else {
2052 retry_counter += 1;
2053 }
2054 }
2055 }
2056 }
2057
2058 if retry_counter >= MAX_CONNECTION_ATTEMPTS {
2059 error!(
2060 "{} streamer failed after {} attempts",
2061 client_type, MAX_CONNECTION_ATTEMPTS
2062 );
2063 return Err(AppError::WebSocketError(format!(
2064 "{} streamer: maximum connection attempts ({}) exceeded",
2065 client_type, MAX_CONNECTION_ATTEMPTS
2066 )));
2067 }
2068
2069 info!("{} streamer connection closed gracefully", client_type);
2070 Ok(())
2071 }
2072
2073 pub async fn disconnect(&mut self) -> Result<(), AppError> {
2087 let mut disconnected = 0;
2088
2089 if let Some(client) = self.market_streamer_client.as_ref() {
2090 let mut client = client.lock().await;
2091 client.disconnect().await;
2092 info!("Market streamer disconnected");
2093 disconnected += 1;
2094 }
2095
2096 if let Some(client) = self.price_streamer_client.as_ref() {
2097 let mut client = client.lock().await;
2098 client.disconnect().await;
2099 info!("Price streamer disconnected");
2100 disconnected += 1;
2101 }
2102
2103 let converter_count = self.converter_tasks.len();
2106 abort_and_drain_tasks(&mut self.converter_tasks).await;
2107 if converter_count > 0 {
2108 debug!("Aborted {} converter task(s)", converter_count);
2109 }
2110
2111 info!("Disconnected {} streaming client(s)", disconnected);
2112 Ok(())
2113 }
2114}
2115
2116#[cfg(feature = "streaming")]
2117impl Drop for StreamerClient {
2118 fn drop(&mut self) {
2122 for handle in self.converter_tasks.drain(..) {
2123 handle.abort();
2124 }
2125 }
2126}
2127
2128#[cfg(test)]
2129mod tests {
2130 use super::is_transient_confirmation_error;
2131 use crate::error::AppError;
2132 use reqwest::StatusCode;
2133
2134 #[test]
2135 fn test_confirmation_error_rate_limit_is_transient() {
2136 assert!(is_transient_confirmation_error(
2137 &AppError::RateLimitExceeded
2138 ));
2139 }
2140
2141 #[test]
2142 fn test_confirmation_error_not_found_is_transient() {
2143 assert!(is_transient_confirmation_error(&AppError::NotFound));
2145 assert!(is_transient_confirmation_error(&AppError::Unexpected(
2146 StatusCode::NOT_FOUND
2147 )));
2148 }
2149
2150 #[test]
2151 fn test_confirmation_error_server_error_is_transient() {
2152 assert!(is_transient_confirmation_error(&AppError::Unexpected(
2153 StatusCode::INTERNAL_SERVER_ERROR
2154 )));
2155 assert!(is_transient_confirmation_error(&AppError::Unexpected(
2156 StatusCode::BAD_GATEWAY
2157 )));
2158 }
2159
2160 #[test]
2161 fn test_confirmation_error_invalid_input_is_permanent() {
2162 assert!(!is_transient_confirmation_error(&AppError::InvalidInput(
2163 "bad".to_string()
2164 )));
2165 }
2166
2167 #[test]
2168 fn test_confirmation_error_auth_and_deser_are_permanent() {
2169 assert!(!is_transient_confirmation_error(&AppError::Unauthorized));
2170 assert!(!is_transient_confirmation_error(
2171 &AppError::OAuthTokenExpired
2172 ));
2173 assert!(!is_transient_confirmation_error(
2174 &AppError::Deserialization("bad".to_string())
2175 ));
2176 assert!(!is_transient_confirmation_error(&AppError::Unexpected(
2177 StatusCode::BAD_REQUEST
2178 )));
2179 }
2180}
2181
2182#[cfg(all(test, feature = "streaming"))]
2186mod streaming_tests {
2187 use super::{
2188 GRACEFUL_CLOSE_MARKER, abort_and_drain_tasks, is_graceful_close, spawn_shutdown_fanout,
2189 };
2190 use lightstreamer_rs::utils::LightstreamerError;
2191 use std::sync::Arc;
2192 use std::time::Duration;
2193 use tokio::sync::Notify;
2194 use tokio::task::JoinHandle;
2195
2196 #[test]
2199 fn test_is_graceful_close_connection_variant_with_marker_is_true() {
2200 let err = LightstreamerError::Connection(format!(
2203 "connection error from server: conerr,-2,{GRACEFUL_CLOSE_MARKER}"
2204 ));
2205 assert!(is_graceful_close(&err));
2206 }
2207
2208 #[test]
2209 fn test_is_graceful_close_protocol_and_invalid_state_with_marker_is_true() {
2210 let protocol = LightstreamerError::Protocol(format!("closing: {GRACEFUL_CLOSE_MARKER}"));
2211 let invalid_state =
2212 LightstreamerError::InvalidState(format!("state: {GRACEFUL_CLOSE_MARKER}"));
2213 assert!(is_graceful_close(&protocol));
2214 assert!(is_graceful_close(&invalid_state));
2215 }
2216
2217 #[test]
2218 fn test_is_graceful_close_connection_variant_without_marker_is_false() {
2219 let err = LightstreamerError::Connection("no message received within 5000 ms".to_string());
2220 assert!(!is_graceful_close(&err));
2221 }
2222
2223 #[test]
2224 fn test_is_graceful_close_other_variant_with_marker_is_false() {
2225 let err = LightstreamerError::Timeout(GRACEFUL_CLOSE_MARKER.to_string());
2228 assert!(!is_graceful_close(&err));
2229 }
2230
2231 #[tokio::test]
2234 async fn test_shutdown_fanout_wakes_all_connection_waiters() {
2235 let source = Arc::new(Notify::new());
2238 let market_signal = Arc::new(Notify::new());
2239 let price_signal = Arc::new(Notify::new());
2240
2241 let fanout = spawn_shutdown_fanout(
2242 Arc::clone(&source),
2243 vec![Arc::clone(&market_signal), Arc::clone(&price_signal)],
2244 );
2245
2246 let market_waiter = tokio::spawn(async move { market_signal.notified().await });
2247 let price_waiter = tokio::spawn(async move { price_signal.notified().await });
2248
2249 source.notify_one();
2254
2255 assert!(
2256 tokio::time::timeout(Duration::from_secs(1), market_waiter)
2257 .await
2258 .is_ok(),
2259 "market connection did not observe the shutdown signal"
2260 );
2261 assert!(
2262 tokio::time::timeout(Duration::from_secs(1), price_waiter)
2263 .await
2264 .is_ok(),
2265 "price connection did not observe the shutdown signal"
2266 );
2267
2268 fanout.abort();
2269 }
2270
2271 #[tokio::test]
2274 async fn test_abort_and_drain_tasks_stops_and_clears() {
2275 let mut tasks: Vec<JoinHandle<()>> = Vec::new();
2276 for _ in 0..3 {
2277 tasks.push(tokio::spawn(async { std::future::pending::<()>().await }));
2279 }
2280 assert!(tasks.iter().all(|h| !h.is_finished()));
2281
2282 abort_and_drain_tasks(&mut tasks).await;
2283
2284 assert!(tasks.is_empty());
2287 }
2288
2289 #[tokio::test]
2290 async fn test_abort_makes_pending_task_finish() {
2291 let handle: JoinHandle<()> = tokio::spawn(async { std::future::pending::<()>().await });
2292 assert!(!handle.is_finished());
2293 handle.abort();
2294 let join_result = handle.await;
2295 assert!(
2296 join_result.is_err(),
2297 "aborted task should yield a cancellation JoinError"
2298 );
2299 }
2300}