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> {
195 let http_client = Arc::new(HttpClient::new_lazy(Config::default())?);
196 Ok(Self { http_client })
197 }
198
199 pub async fn ws_info(&self) -> Result<WebsocketInfo, AppError> {
213 self.http_client.ws_info().await
214 }
215
216 #[deprecated(
221 note = "use ws_info() which reuses the cached session and returns a typed error instead of a default-on-error WebsocketInfo"
222 )]
223 pub async fn get_ws_info(&self) -> WebsocketInfo {
224 self.ws_info().await.unwrap_or_default()
225 }
226}
227
228#[async_trait]
229impl MarketService for Client {
230 async fn search_markets(&self, search_term: &str) -> Result<MarketSearchResponse, AppError> {
231 let path = format!("markets?searchTerm={}", search_term);
232 info!("Searching markets with term: {}", search_term);
233 let result: MarketSearchResponse = self.http_client.get(&path, Some(1)).await?;
234 debug!("{} markets found", result.markets.len());
235 Ok(result)
236 }
237
238 async fn get_market_details(&self, epic: &str) -> Result<MarketDetails, AppError> {
239 let path = format!("markets/{epic}");
240 info!("Getting market details: {}", epic);
241 let market_details: MarketDetails = self.http_client.get(&path, Some(3)).await?;
245 debug!("Market details obtained for: {}", epic);
246 Ok(market_details)
247 }
248
249 async fn get_multiple_market_details(
250 &self,
251 epics: &[String],
252 ) -> Result<MultipleMarketDetailsResponse, AppError> {
253 if epics.is_empty() {
254 return Ok(MultipleMarketDetailsResponse::default());
255 } else if epics.len() > 50 {
256 return Err(AppError::InvalidInput(
257 "The maximum number of EPICs is 50".to_string(),
258 ));
259 }
260
261 let epics_str = epics.join(",");
262 let path = format!("markets?epics={}", epics_str);
263 debug!(
264 "Getting market details for {} EPICs in a batch",
265 epics.len()
266 );
267
268 let response: MultipleMarketDetailsResponse = self.http_client.get(&path, Some(2)).await?;
269
270 Ok(response)
271 }
272
273 async fn get_historical_prices(
274 &self,
275 epic: &str,
276 resolution: &str,
277 from: &str,
278 to: &str,
279 ) -> Result<HistoricalPricesResponse, AppError> {
280 let path = format!(
281 "prices/{}?resolution={}&from={}&to={}",
282 epic, resolution, from, to
283 );
284 info!("Getting historical prices for: {}", epic);
285 let result: HistoricalPricesResponse = self.http_client.get(&path, Some(3)).await?;
286 debug!("Historical prices obtained for: {}", epic);
287 Ok(result)
288 }
289
290 async fn get_historical_prices_by_date_range(
291 &self,
292 epic: &str,
293 resolution: &str,
294 start_date: &str,
295 end_date: &str,
296 ) -> Result<HistoricalPricesResponse, AppError> {
297 let path = format!("prices/{}/{}/{}/{}", epic, resolution, start_date, end_date);
298 info!(
299 "Getting historical prices for epic: {}, resolution: {}, from: {} to: {}",
300 epic, resolution, start_date, end_date
301 );
302 let result: HistoricalPricesResponse = self.http_client.get(&path, Some(2)).await?;
303 debug!(
304 "Historical prices obtained for epic: {}, {} data points",
305 epic,
306 result.prices.len()
307 );
308 Ok(result)
309 }
310
311 async fn get_recent_prices(
312 &self,
313 params: &RecentPricesRequest<'_>,
314 ) -> Result<HistoricalPricesResponse, AppError> {
315 let mut query_params = Vec::new();
316
317 if let Some(res) = params.resolution {
318 query_params.push(format!("resolution={}", res));
319 }
320 if let Some(f) = params.from {
321 query_params.push(format!("from={}", f));
322 }
323 if let Some(t) = params.to {
324 query_params.push(format!("to={}", t));
325 }
326 if let Some(max) = params.max_points {
327 query_params.push(format!("max={}", max));
328 }
329 if let Some(size) = params.page_size {
330 query_params.push(format!("pageSize={}", size));
331 }
332 if let Some(num) = params.page_number {
333 query_params.push(format!("pageNumber={}", num));
334 }
335
336 let query_string = if query_params.is_empty() {
337 String::new()
338 } else {
339 format!("?{}", query_params.join("&"))
340 };
341
342 let path = format!("prices/{}{}", params.epic, query_string);
343 info!("Getting recent prices for epic: {}", params.epic);
344 let result: HistoricalPricesResponse = self.http_client.get(&path, Some(3)).await?;
345 debug!(
346 "Recent prices obtained for epic: {}, {} data points",
347 params.epic,
348 result.prices.len()
349 );
350 Ok(result)
351 }
352
353 async fn get_historical_prices_by_count_v1(
354 &self,
355 epic: &str,
356 resolution: &str,
357 num_points: u32,
358 ) -> Result<HistoricalPricesResponse, AppError> {
359 let path = format!("prices/{}/{}/{}", epic, resolution, num_points);
360 info!(
361 "Getting historical prices (v1) for epic: {}, resolution: {}, points: {}",
362 epic, resolution, num_points
363 );
364 let result: HistoricalPricesResponse = self.http_client.get(&path, Some(1)).await?;
365 debug!(
366 "Historical prices (v1) obtained for epic: {}, {} data points",
367 epic,
368 result.prices.len()
369 );
370 Ok(result)
371 }
372
373 async fn get_historical_prices_by_count_v2(
374 &self,
375 epic: &str,
376 resolution: &str,
377 num_points: u32,
378 ) -> Result<HistoricalPricesResponse, AppError> {
379 let path = format!("prices/{}/{}/{}", epic, resolution, num_points);
380 info!(
381 "Getting historical prices (v2) for epic: {}, resolution: {}, points: {}",
382 epic, resolution, num_points
383 );
384 let result: HistoricalPricesResponse = self.http_client.get(&path, Some(2)).await?;
385 debug!(
386 "Historical prices (v2) obtained for epic: {}, {} data points",
387 epic,
388 result.prices.len()
389 );
390 Ok(result)
391 }
392
393 async fn get_market_navigation(&self) -> Result<MarketNavigationResponse, AppError> {
394 let path = "marketnavigation";
395 info!("Getting top-level market navigation nodes");
396 let result: MarketNavigationResponse = self.http_client.get(path, Some(1)).await?;
397 debug!("{} navigation nodes found", result.nodes.len());
398 debug!("{} markets found at root level", result.markets.len());
399 Ok(result)
400 }
401
402 async fn get_market_navigation_node(
403 &self,
404 node_id: &str,
405 ) -> Result<MarketNavigationResponse, AppError> {
406 let path = format!("marketnavigation/{}", node_id);
407 info!("Getting market navigation node: {}", node_id);
408 let result: MarketNavigationResponse = self.http_client.get(&path, Some(1)).await?;
409 debug!("{} child nodes found", result.nodes.len());
410 debug!("{} markets found in node {}", result.markets.len(), node_id);
411 Ok(result)
412 }
413
414 async fn get_all_markets(&self) -> Result<Vec<MarketData>, AppError> {
415 let max_depth = 6;
416 info!(
417 "Starting comprehensive market hierarchy traversal (max {} levels)",
418 max_depth
419 );
420
421 let root_response = self.get_market_navigation().await?;
422 info!(
423 "Root navigation: {} nodes, {} markets at top level",
424 root_response.nodes.len(),
425 root_response.markets.len()
426 );
427
428 let mut seen_epics: HashSet<String> = HashSet::new();
432 let mut all_markets: Vec<MarketData> = Vec::new();
433 for market in root_response.markets {
434 if seen_epics.insert(market.epic.clone()) {
435 all_markets.push(market);
436 }
437 }
438 let mut nodes_to_process = root_response.nodes;
439 let mut processed_levels = 0;
440
441 while !nodes_to_process.is_empty() && processed_levels < max_depth {
442 let mut next_level_nodes = Vec::new();
443 let mut level_market_count = 0;
444
445 info!(
446 "Processing level {} with {} nodes",
447 processed_levels,
448 nodes_to_process.len()
449 );
450
451 for node in &nodes_to_process {
452 match self.get_market_navigation_node(&node.id).await {
453 Ok(node_response) => {
454 let node_markets = node_response.markets.len();
455 let node_children = node_response.nodes.len();
456
457 if node_markets > 0 || node_children > 0 {
458 debug!(
459 "Node '{}' (level {}): {} markets, {} child nodes",
460 node.name, processed_levels, node_markets, node_children
461 );
462 }
463
464 for market in node_response.markets {
467 if seen_epics.insert(market.epic.clone()) {
468 all_markets.push(market);
469 level_market_count += 1;
470 }
471 }
472 next_level_nodes.extend(node_response.nodes);
473 }
474 Err(e) => {
475 tracing::error!(
476 "Failed to get markets for node '{}' at level {}: {:?}",
477 node.name,
478 processed_levels,
479 e
480 );
481 }
482 }
483 }
484
485 info!(
486 "Level {} completed: {} markets found, {} nodes for next level",
487 processed_levels,
488 level_market_count,
489 next_level_nodes.len()
490 );
491
492 nodes_to_process = next_level_nodes;
493 processed_levels += 1;
494 }
495
496 info!(
497 "Market hierarchy traversal completed: {} total markets found across {} levels",
498 all_markets.len(),
499 processed_levels
500 );
501
502 Ok(all_markets)
503 }
504
505 async fn get_vec_db_entries(&self) -> Result<Vec<DBEntryResponse>, AppError> {
506 info!("Getting all markets from hierarchy for DB entries");
507
508 let all_markets = self.get_all_markets().await?;
509 info!("Collected {} markets from hierarchy", all_markets.len());
510
511 let mut vec_db_entries: Vec<DBEntryResponse> = all_markets
512 .iter()
513 .map(DBEntryResponse::from)
514 .filter(|entry| !entry.epic.is_empty())
515 .collect();
516
517 info!("Created {} DB entries from markets", vec_db_entries.len());
518
519 let mut symbol_info: std::collections::HashMap<String, (String, String)> =
525 std::collections::HashMap::new();
526 for entry in &vec_db_entries {
527 if entry.symbol.is_empty() || entry.epic.is_empty() {
528 continue;
529 }
530 symbol_info
531 .entry(entry.symbol.clone())
532 .or_insert_with(|| (entry.epic.clone(), entry.expiry.clone()));
533 }
534
535 info!(
536 "Found {} unique symbols to fetch expiry dates for",
537 symbol_info.len()
538 );
539
540 let symbol_expiry_map: std::collections::HashMap<String, String> =
544 futures::stream::iter(symbol_info)
545 .map(|(symbol, (epic, fallback_expiry))| async move {
546 match self.get_market_details(&epic).await {
547 Ok(market_details) => {
548 let expiry_date = market_details
549 .instrument
550 .expiry_details
551 .as_ref()
552 .map(|details| details.last_dealing_date.clone())
553 .unwrap_or_else(|| market_details.instrument.expiry.clone());
554
555 info!(
556 symbol = %symbol,
557 expiry = %expiry_date,
558 "fetched expiry date for symbol"
559 );
560 (symbol, expiry_date)
561 }
562 Err(e) => {
563 tracing::error!(
564 "Failed to get market details for epic {} (symbol {}): {:?}",
565 epic,
566 symbol,
567 e
568 );
569 (symbol, fallback_expiry)
570 }
571 }
572 })
573 .buffer_unordered(MARKET_DETAILS_CONCURRENCY)
574 .collect()
575 .await;
576
577 for entry in &mut vec_db_entries {
578 if let Some(expiry_date) = symbol_expiry_map.get(&entry.symbol) {
579 entry.expiry = expiry_date.clone();
580 }
581 }
582
583 info!("Updated expiry dates for {} entries", vec_db_entries.len());
584 Ok(vec_db_entries)
585 }
586
587 async fn get_categories(&self) -> Result<CategoriesResponse, AppError> {
588 info!("Getting all categories of instruments");
589 let result: CategoriesResponse = self.http_client.get("categories", Some(1)).await?;
590 debug!("{} categories found", result.categories.len());
591 Ok(result)
592 }
593
594 async fn get_category_instruments(
595 &self,
596 category_id: &str,
597 page_number: Option<u32>,
598 page_size: Option<u32>,
599 ) -> Result<CategoryInstrumentsResponse, AppError> {
600 let mut path = format!("categories/{}/instruments", category_id);
601
602 let mut query_params = Vec::new();
603 if let Some(page) = page_number {
604 query_params.push(format!("pageNumber={}", page));
605 }
606 if let Some(size) = page_size {
607 if size > 1000 {
608 return Err(AppError::InvalidInput(
609 "pageSize cannot exceed 1000".to_string(),
610 ));
611 }
612 query_params.push(format!("pageSize={}", size));
613 }
614
615 if !query_params.is_empty() {
616 path = format!("{}?{}", path, query_params.join("&"));
617 }
618
619 info!(
620 "Getting instruments for category: {} (page: {:?}, size: {:?})",
621 category_id, page_number, page_size
622 );
623 let result: CategoryInstrumentsResponse = self.http_client.get(&path, Some(1)).await?;
624 debug!(
625 "{} instruments found in category {}",
626 result.instruments.len(),
627 category_id
628 );
629 Ok(result)
630 }
631}
632
633#[async_trait]
634impl AccountService for Client {
635 async fn get_accounts(&self) -> Result<AccountsResponse, AppError> {
636 info!("Getting account information");
637 let result: AccountsResponse = self.http_client.get("accounts", Some(1)).await?;
638 debug!(
639 "Account information obtained: {} accounts",
640 result.accounts.len()
641 );
642 Ok(result)
643 }
644
645 async fn get_positions(&self) -> Result<PositionsResponse, AppError> {
646 debug!("Getting open positions");
647 let result: PositionsResponse = self.http_client.get("positions", Some(2)).await?;
648 debug!("Positions obtained: {} positions", result.positions.len());
649 Ok(result)
650 }
651
652 async fn get_positions_w_filter(&self, filter: &str) -> Result<PositionsResponse, AppError> {
653 debug!("Getting open positions with filter: {}", filter);
654 let mut positions = self.get_positions().await?;
655
656 positions
657 .positions
658 .retain(|position| position.market.epic.contains(filter));
659
660 debug!(
661 "Positions obtained after filtering: {} positions",
662 positions.positions.len()
663 );
664 Ok(positions)
665 }
666
667 async fn get_working_orders(&self) -> Result<WorkingOrdersResponse, AppError> {
668 info!("Getting working orders");
669 let result: WorkingOrdersResponse = self.http_client.get("workingorders", Some(2)).await?;
670 debug!(
671 "Working orders obtained: {} orders",
672 result.working_orders.len()
673 );
674 Ok(result)
675 }
676
677 async fn get_activity(
678 &self,
679 from: &str,
680 to: &str,
681 ) -> Result<AccountActivityResponse, AppError> {
682 let path = format!("history/activity?from={}&to={}&pageSize=500", from, to);
683 info!("Getting account activity");
684 let result: AccountActivityResponse = self.http_client.get(&path, Some(3)).await?;
685 debug!(
686 "Account activity obtained: {} activities",
687 result.activities.len()
688 );
689 Ok(result)
690 }
691
692 async fn get_activity_with_details(
693 &self,
694 from: &str,
695 to: &str,
696 ) -> Result<AccountActivityResponse, AppError> {
697 let path = format!(
698 "history/activity?from={}&to={}&detailed=true&pageSize=500",
699 from, to
700 );
701 info!("Getting detailed account activity");
702 let result: AccountActivityResponse = self.http_client.get(&path, Some(3)).await?;
703 debug!(
704 "Detailed account activity obtained: {} activities",
705 result.activities.len()
706 );
707 Ok(result)
708 }
709
710 async fn get_transactions(
711 &self,
712 from: &str,
713 to: &str,
714 ) -> Result<TransactionHistoryResponse, AppError> {
715 const PAGE_SIZE: u32 = 200;
716 let mut all_transactions = Vec::new();
717 let mut current_page = 1;
718 #[allow(unused_assignments)]
719 let mut last_metadata = None;
720
721 loop {
722 let path = format!(
723 "history/transactions?from={}&to={}&pageSize={}&pageNumber={}",
724 from, to, PAGE_SIZE, current_page
725 );
726 info!("Getting transaction history page {}", current_page);
727
728 let result: TransactionHistoryResponse = self.http_client.get(&path, Some(2)).await?;
729
730 let total_pages = result.metadata.page_data.total_pages as u32;
731 last_metadata = Some(result.metadata);
732 all_transactions.extend(result.transactions);
733
734 if current_page >= total_pages {
735 break;
736 }
737 current_page += 1;
738 }
739
740 debug!(
741 "Total transaction history obtained: {} transactions",
742 all_transactions.len()
743 );
744
745 Ok(TransactionHistoryResponse {
746 transactions: all_transactions,
747 metadata: last_metadata
748 .ok_or_else(|| AppError::InvalidInput("Could not retrieve metadata".to_string()))?,
749 })
750 }
751
752 async fn get_preferences(&self) -> Result<AccountPreferencesResponse, AppError> {
753 info!("Getting account preferences");
754 let result: AccountPreferencesResponse = self
755 .http_client
756 .get("accounts/preferences", Some(1))
757 .await?;
758 debug!(
759 "Account preferences obtained: trailing_stops_enabled={}",
760 result.trailing_stops_enabled
761 );
762 Ok(result)
763 }
764
765 async fn update_preferences(&self, trailing_stops_enabled: bool) -> Result<(), AppError> {
766 info!(
767 "Updating account preferences: trailing_stops_enabled={}",
768 trailing_stops_enabled
769 );
770 let request = serde_json::json!({
771 "trailingStopsEnabled": trailing_stops_enabled
772 });
773 let _: serde_json::Value = self
774 .http_client
775 .put("accounts/preferences", &request, Some(1))
776 .await?;
777 debug!("Account preferences updated");
778 Ok(())
779 }
780
781 async fn get_activity_by_period(
782 &self,
783 period_ms: u64,
784 ) -> Result<AccountActivityResponse, AppError> {
785 let path = format!("history/activity/{}", period_ms);
786 info!("Getting account activity for period: {} ms", period_ms);
787 let result: AccountActivityResponse = self.http_client.get(&path, Some(1)).await?;
788 debug!(
789 "Account activity obtained: {} activities",
790 result.activities.len()
791 );
792 Ok(result)
793 }
794}
795
796#[async_trait]
797impl OrderService for Client {
798 async fn create_order(
799 &self,
800 order: &CreateOrderRequest,
801 ) -> Result<CreateOrderResponse, AppError> {
802 info!("Creating order for: {}", order.epic);
803 let result: CreateOrderResponse = self
804 .http_client
805 .post("positions/otc", order, Some(2))
806 .await?;
807 debug!("Order created with reference: {}", result.deal_reference);
808 Ok(result)
809 }
810
811 async fn get_order_confirmation(
812 &self,
813 deal_reference: &str,
814 ) -> Result<OrderConfirmationResponse, AppError> {
815 let path = format!("confirms/{}", deal_reference);
816 info!("Getting confirmation for order: {}", deal_reference);
817 let result: OrderConfirmationResponse = self.http_client.get(&path, Some(1)).await?;
818 debug!("Confirmation obtained for order: {}", deal_reference);
819 Ok(result)
820 }
821
822 async fn get_order_confirmation_w_retry(
823 &self,
824 deal_reference: &str,
825 retries: u64,
826 delay_ms: u64,
827 ) -> Result<OrderConfirmationResponse, AppError> {
828 let base = Duration::from_millis(delay_ms);
831 let mut attempt: u32 = 0;
832 loop {
833 match self.get_order_confirmation(deal_reference).await {
834 Ok(response) => return Ok(response),
835 Err(e) => {
836 if !is_transient_confirmation_error(&e) {
839 return Err(e);
840 }
841 if u64::from(attempt) >= retries {
842 return Err(e);
843 }
844 let delay = backoff_delay(base, attempt);
845 let delay_ms = u64::try_from(delay.as_millis()).unwrap_or(u64::MAX);
846 let next_attempt = attempt.checked_add(1).ok_or_else(|| {
847 AppError::Generic("retry attempt counter overflow".to_string())
848 })?;
849 warn!(
850 deal_reference = %deal_reference,
851 attempt = next_attempt,
852 max_retries = retries,
853 delay_ms,
854 "retrying order confirmation after transient error"
855 );
856 sleep(delay).await;
857 attempt = next_attempt;
858 }
859 }
860 }
861 }
862
863 async fn update_position(
864 &self,
865 deal_id: &str,
866 update: &UpdatePositionRequest,
867 ) -> Result<UpdatePositionResponse, AppError> {
868 let path = format!("positions/otc/{}", deal_id);
869 info!("Updating position: {}", deal_id);
870 let result: UpdatePositionResponse = self.http_client.put(&path, update, Some(2)).await?;
871 debug!(
872 "Position updated: {} with deal reference: {}",
873 deal_id, result.deal_reference
874 );
875 Ok(result)
876 }
877
878 async fn update_level_in_position(
879 &self,
880 deal_id: &str,
881 limit_level: Option<f64>,
882 ) -> Result<UpdatePositionResponse, AppError> {
883 let path = format!("positions/otc/{}", deal_id);
884 info!("Updating position: {}", deal_id);
885 let limit_level = limit_level.unwrap_or(0.0);
886
887 let update: UpdatePositionRequest = UpdatePositionRequest {
888 guaranteed_stop: None,
889 limit_level: Some(limit_level),
890 stop_level: None,
891 trailing_stop: None,
892 trailing_stop_distance: None,
893 trailing_stop_increment: None,
894 };
895 let result: UpdatePositionResponse = self.http_client.put(&path, update, Some(2)).await?;
896 debug!(
897 "Position updated: {} with deal reference: {}",
898 deal_id, result.deal_reference
899 );
900 Ok(result)
901 }
902
903 async fn close_position(
904 &self,
905 close_request: &ClosePositionRequest,
906 ) -> Result<ClosePositionResponse, AppError> {
907 info!("Closing position");
908
909 let result: ClosePositionResponse = self
912 .http_client
913 .post_with_delete_method("positions/otc", close_request, Some(1))
914 .await?;
915
916 debug!("Position closed with reference: {}", result.deal_reference);
917 Ok(result)
918 }
919
920 async fn create_working_order(
921 &self,
922 order: &CreateWorkingOrderRequest,
923 ) -> Result<CreateWorkingOrderResponse, AppError> {
924 info!("Creating working order for: {}", order.epic);
925 let result: CreateWorkingOrderResponse = self
926 .http_client
927 .post("workingorders/otc", order, Some(2))
928 .await?;
929 debug!(
930 "Working order created with reference: {}",
931 result.deal_reference
932 );
933 Ok(result)
934 }
935
936 async fn delete_working_order(&self, deal_id: &str) -> Result<(), AppError> {
937 let path = format!("workingorders/otc/{}", deal_id);
938 let result: CreateWorkingOrderResponse =
939 self.http_client.delete(path.as_str(), Some(2)).await?;
940 debug!(
941 "Working order created with reference: {}",
942 result.deal_reference
943 );
944 Ok(())
945 }
946
947 async fn get_position(&self, deal_id: &str) -> Result<SinglePositionResponse, AppError> {
948 let path = format!("positions/{}", deal_id);
949 info!("Getting position: {}", deal_id);
950 let result: SinglePositionResponse = self.http_client.get(&path, Some(2)).await?;
951 debug!("Position obtained for deal: {}", deal_id);
952 Ok(result)
953 }
954
955 async fn update_working_order(
956 &self,
957 deal_id: &str,
958 update: &UpdateWorkingOrderRequest,
959 ) -> Result<CreateWorkingOrderResponse, AppError> {
960 let path = format!("workingorders/otc/{}", deal_id);
961 info!("Updating working order: {}", deal_id);
962 let result: CreateWorkingOrderResponse =
963 self.http_client.put(&path, update, Some(2)).await?;
964 debug!(
965 "Working order updated: {} with reference: {}",
966 deal_id, result.deal_reference
967 );
968 Ok(result)
969 }
970}
971
972#[async_trait]
977impl WatchlistService for Client {
978 async fn get_watchlists(&self) -> Result<WatchlistsResponse, AppError> {
979 info!("Getting all watchlists");
980 let result: WatchlistsResponse = self.http_client.get("watchlists", Some(1)).await?;
981 debug!(
982 "Watchlists obtained: {} watchlists",
983 result.watchlists.len()
984 );
985 Ok(result)
986 }
987
988 async fn create_watchlist(
989 &self,
990 name: &str,
991 epics: Option<&[String]>,
992 ) -> Result<CreateWatchlistResponse, AppError> {
993 info!("Creating watchlist: {}", name);
994 let request = CreateWatchlistRequest {
995 name: name.to_string(),
996 epics: epics.map(|e| e.to_vec()),
997 };
998 let result: CreateWatchlistResponse = self
999 .http_client
1000 .post("watchlists", &request, Some(1))
1001 .await?;
1002 debug!(
1003 "Watchlist created: {} with ID: {}",
1004 name, result.watchlist_id
1005 );
1006 Ok(result)
1007 }
1008
1009 async fn get_watchlist(
1010 &self,
1011 watchlist_id: &str,
1012 ) -> Result<WatchlistMarketsResponse, AppError> {
1013 let path = format!("watchlists/{}", watchlist_id);
1014 info!("Getting watchlist: {}", watchlist_id);
1015 let result: WatchlistMarketsResponse = self.http_client.get(&path, Some(1)).await?;
1016 debug!(
1017 "Watchlist obtained: {} with {} markets",
1018 watchlist_id,
1019 result.markets.len()
1020 );
1021 Ok(result)
1022 }
1023
1024 async fn delete_watchlist(&self, watchlist_id: &str) -> Result<StatusResponse, AppError> {
1025 let path = format!("watchlists/{}", watchlist_id);
1026 info!("Deleting watchlist: {}", watchlist_id);
1027 let result: StatusResponse = self.http_client.delete(&path, Some(1)).await?;
1028 debug!("Watchlist deleted: {}", watchlist_id);
1029 Ok(result)
1030 }
1031
1032 async fn add_to_watchlist(
1033 &self,
1034 watchlist_id: &str,
1035 epic: &str,
1036 ) -> Result<StatusResponse, AppError> {
1037 let path = format!("watchlists/{}", watchlist_id);
1038 info!("Adding {} to watchlist: {}", epic, watchlist_id);
1039 let request = AddToWatchlistRequest {
1040 epic: epic.to_string(),
1041 };
1042 let result: StatusResponse = self.http_client.put(&path, &request, Some(1)).await?;
1043 debug!("Added {} to watchlist: {}", epic, watchlist_id);
1044 Ok(result)
1045 }
1046
1047 async fn remove_from_watchlist(
1048 &self,
1049 watchlist_id: &str,
1050 epic: &str,
1051 ) -> Result<StatusResponse, AppError> {
1052 let path = format!("watchlists/{}/{}", watchlist_id, epic);
1053 info!("Removing {} from watchlist: {}", epic, watchlist_id);
1054 let result: StatusResponse = self.http_client.delete(&path, Some(1)).await?;
1055 debug!("Removed {} from watchlist: {}", epic, watchlist_id);
1056 Ok(result)
1057 }
1058}
1059
1060#[async_trait]
1065impl SentimentService for Client {
1066 async fn get_client_sentiment(
1067 &self,
1068 market_ids: &[String],
1069 ) -> Result<ClientSentimentResponse, AppError> {
1070 let market_ids_str = market_ids.join(",");
1071 let path = format!("clientsentiment?marketIds={}", market_ids_str);
1072 info!("Getting client sentiment for {} markets", market_ids.len());
1073 let result: ClientSentimentResponse = self.http_client.get(&path, Some(1)).await?;
1074 debug!(
1075 "Client sentiment obtained for {} markets",
1076 result.client_sentiments.len()
1077 );
1078 Ok(result)
1079 }
1080
1081 async fn get_client_sentiment_by_market(
1082 &self,
1083 market_id: &str,
1084 ) -> Result<MarketSentiment, AppError> {
1085 let path = format!("clientsentiment/{}", market_id);
1086 info!("Getting client sentiment for market: {}", market_id);
1087 let result: MarketSentiment = self.http_client.get(&path, Some(1)).await?;
1088 debug!(
1089 "Client sentiment for {}: {}% long, {}% short",
1090 market_id, result.long_position_percentage, result.short_position_percentage
1091 );
1092 Ok(result)
1093 }
1094
1095 async fn get_related_sentiment(
1096 &self,
1097 market_id: &str,
1098 ) -> Result<ClientSentimentResponse, AppError> {
1099 let path = format!("clientsentiment/related/{}", market_id);
1100 info!("Getting related sentiment for market: {}", market_id);
1101 let result: ClientSentimentResponse = self.http_client.get(&path, Some(1)).await?;
1102 debug!(
1103 "Related sentiment obtained: {} markets",
1104 result.client_sentiments.len()
1105 );
1106 Ok(result)
1107 }
1108}
1109
1110#[async_trait]
1115impl CostsService for Client {
1116 async fn get_indicative_costs_open(
1117 &self,
1118 request: &OpenCostsRequest,
1119 ) -> Result<IndicativeCostsResponse, AppError> {
1120 info!(
1121 "Getting indicative costs for opening position on: {}",
1122 request.epic
1123 );
1124 let result: IndicativeCostsResponse = self
1125 .http_client
1126 .post("indicativecostsandcharges/open", request, Some(1))
1127 .await?;
1128 debug!(
1129 "Indicative costs obtained, reference: {}",
1130 result.indicative_quote_reference
1131 );
1132 Ok(result)
1133 }
1134
1135 async fn get_indicative_costs_close(
1136 &self,
1137 request: &CloseCostsRequest,
1138 ) -> Result<IndicativeCostsResponse, AppError> {
1139 info!(
1140 "Getting indicative costs for closing position: {}",
1141 request.deal_id
1142 );
1143 let result: IndicativeCostsResponse = self
1144 .http_client
1145 .post("indicativecostsandcharges/close", request, Some(1))
1146 .await?;
1147 debug!(
1148 "Indicative costs obtained, reference: {}",
1149 result.indicative_quote_reference
1150 );
1151 Ok(result)
1152 }
1153
1154 async fn get_indicative_costs_edit(
1155 &self,
1156 request: &EditCostsRequest,
1157 ) -> Result<IndicativeCostsResponse, AppError> {
1158 info!(
1159 "Getting indicative costs for editing position: {}",
1160 request.deal_id
1161 );
1162 let result: IndicativeCostsResponse = self
1163 .http_client
1164 .post("indicativecostsandcharges/edit", request, Some(1))
1165 .await?;
1166 debug!(
1167 "Indicative costs obtained, reference: {}",
1168 result.indicative_quote_reference
1169 );
1170 Ok(result)
1171 }
1172
1173 async fn get_costs_history(
1174 &self,
1175 from: &str,
1176 to: &str,
1177 ) -> Result<CostsHistoryResponse, AppError> {
1178 let path = format!("indicativecostsandcharges/history/from/{}/to/{}", from, to);
1179 info!("Getting costs history from {} to {}", from, to);
1180 let result: CostsHistoryResponse = self.http_client.get(&path, Some(1)).await?;
1181 debug!("Costs history obtained: {} entries", result.costs.len());
1182 Ok(result)
1183 }
1184
1185 async fn get_durable_medium(
1186 &self,
1187 quote_reference: &str,
1188 ) -> Result<DurableMediumResponse, AppError> {
1189 let path = format!(
1190 "indicativecostsandcharges/durablemedium/{}",
1191 quote_reference
1192 );
1193 info!("Getting durable medium for reference: {}", quote_reference);
1194 let result: DurableMediumResponse = self.http_client.get(&path, Some(1)).await?;
1195 debug!("Durable medium obtained for reference: {}", quote_reference);
1196 Ok(result)
1197 }
1198}
1199
1200#[async_trait]
1205impl OperationsService for Client {
1206 async fn get_client_apps(&self) -> Result<ApplicationDetailsResponse, AppError> {
1207 info!("Getting client applications");
1208 let result: ApplicationDetailsResponse = self
1209 .http_client
1210 .get("operations/application", Some(1))
1211 .await?;
1212 debug!(
1214 name = ?result.name,
1215 status = %result.status,
1216 "Client application obtained"
1217 );
1218 Ok(result)
1219 }
1220
1221 async fn disable_client_app(&self) -> Result<StatusResponse, AppError> {
1222 info!("Disabling current client application");
1223 let result: StatusResponse = self
1224 .http_client
1225 .put(
1226 "operations/application/disable",
1227 &serde_json::json!({}),
1228 Some(1),
1229 )
1230 .await?;
1231 debug!("Client application disabled");
1232 Ok(result)
1233 }
1234}
1235
1236pub struct StreamerClient {
1246 account_id: String,
1247 market_streamer_client: Option<Arc<Mutex<LightstreamerClient>>>,
1248 price_streamer_client: Option<Arc<Mutex<LightstreamerClient>>>,
1249 has_market_stream_subs: bool,
1251 has_price_stream_subs: bool,
1252 converter_tasks: Vec<JoinHandle<()>>,
1258}
1259
1260impl StreamerClient {
1261 pub async fn new() -> Result<Self, AppError> {
1276 let client = Client::try_new()?;
1277 Self::with_client(&client).await
1278 }
1279
1280 pub async fn with_client(client: &Client) -> Result<Self, AppError> {
1294 let ws_info = client.ws_info().await?;
1295 let password = ws_info.get_ws_password();
1296
1297 let market_streamer_client = Arc::new(Mutex::new(LightstreamerClient::new(
1299 Some(ws_info.server.as_str()),
1300 None,
1301 Some(&ws_info.account_id),
1302 Some(&password),
1303 )?));
1304
1305 let price_streamer_client = Arc::new(Mutex::new(LightstreamerClient::new(
1306 Some(ws_info.server.as_str()),
1307 None,
1308 Some(&ws_info.account_id),
1309 Some(&password),
1310 )?));
1311
1312 {
1315 let mut streamer = market_streamer_client.lock().await;
1316 streamer
1317 .connection_options
1318 .set_forced_transport(Some(Transport::WsStreaming));
1319 streamer.set_logging_type(LogType::TracingLogs);
1320 }
1321 {
1322 let mut streamer = price_streamer_client.lock().await;
1323 streamer
1324 .connection_options
1325 .set_forced_transport(Some(Transport::WsStreaming));
1326 streamer.set_logging_type(LogType::TracingLogs);
1327 }
1328
1329 Ok(Self {
1330 account_id: ws_info.account_id.clone(),
1331 market_streamer_client: Some(market_streamer_client),
1332 price_streamer_client: Some(price_streamer_client),
1333 has_market_stream_subs: false,
1334 has_price_stream_subs: false,
1335 converter_tasks: Vec::new(),
1336 })
1337 }
1338
1339 pub async fn market_subscribe(
1369 &mut self,
1370 epics: Vec<String>,
1371 fields: HashSet<StreamingMarketField>,
1372 ) -> Result<mpsc::UnboundedReceiver<PriceData>, AppError> {
1373 self.has_market_stream_subs = true;
1375
1376 let fields = get_streaming_market_fields(&fields);
1377 let market_epics: Vec<String> = epics
1378 .iter()
1379 .map(|epic| "MARKET:".to_string() + epic)
1380 .collect();
1381 let mut subscription =
1382 Subscription::new(SubscriptionMode::Merge, Some(market_epics), Some(fields))?;
1383
1384 subscription.set_data_adapter(None)?;
1385 subscription.set_requested_snapshot(Some(Snapshot::Yes))?;
1386
1387 let (listener, item_receiver) = ChannelSubscriptionListener::create_channel();
1389 subscription.add_listener(Box::new(listener));
1390
1391 let client = self.market_streamer_client.as_ref().ok_or_else(|| {
1393 AppError::WebSocketError("market streamer client not initialized".to_string())
1394 })?;
1395
1396 {
1397 let mut client = client.lock().await;
1398 client
1399 .connection_options
1400 .set_forced_transport(Some(Transport::WsStreaming));
1401 LightstreamerClient::subscribe(client.subscription_sender.clone(), subscription)
1402 .await?;
1403 }
1404
1405 let (price_tx, price_rx) = mpsc::unbounded_channel();
1407 let handle = tokio::spawn(async move {
1408 let mut receiver = item_receiver;
1409 while let Some(item_update) = receiver.recv().await {
1410 let price_data = PriceData::from(&item_update);
1411 if price_tx.send(price_data).is_err() {
1412 tracing::debug!("Price channel receiver dropped");
1413 break;
1414 }
1415 }
1416 });
1417 self.converter_tasks.push(handle);
1419
1420 info!(
1421 "Market subscription created for {} instruments",
1422 epics.len()
1423 );
1424 Ok(price_rx)
1425 }
1426
1427 pub async fn trade_subscribe(
1450 &mut self,
1451 ) -> Result<mpsc::UnboundedReceiver<TradeFields>, AppError> {
1452 self.has_market_stream_subs = true;
1454
1455 let account_id = self.account_id.clone();
1456 let fields = Some(vec![
1457 "CONFIRMS".to_string(),
1458 "OPU".to_string(),
1459 "WOU".to_string(),
1460 ]);
1461 let trade_items = vec![format!("TRADE:{account_id}")];
1462
1463 let mut subscription =
1464 Subscription::new(SubscriptionMode::Distinct, Some(trade_items), 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 (trade_tx, trade_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 trade_data = crate::presentation::trade::TradeData::from(&item_update);
1493 if trade_tx.send(trade_data.fields).is_err() {
1494 tracing::debug!("Trade channel receiver dropped");
1495 break;
1496 }
1497 }
1498 });
1499 self.converter_tasks.push(handle);
1501
1502 info!("Trade subscription created for account: {}", account_id);
1503 Ok(trade_rx)
1504 }
1505
1506 pub async fn account_subscribe(
1533 &mut self,
1534 fields: HashSet<StreamingAccountDataField>,
1535 ) -> Result<mpsc::UnboundedReceiver<AccountFields>, AppError> {
1536 self.has_market_stream_subs = true;
1538
1539 let fields = get_streaming_account_data_fields(&fields);
1540 let account_id = self.account_id.clone();
1541 let account_items = vec![format!("ACCOUNT:{account_id}")];
1542
1543 let mut subscription =
1544 Subscription::new(SubscriptionMode::Merge, Some(account_items), Some(fields))?;
1545
1546 subscription.set_data_adapter(None)?;
1547 subscription.set_requested_snapshot(Some(Snapshot::Yes))?;
1548
1549 let (listener, item_receiver) = ChannelSubscriptionListener::create_channel();
1551 subscription.add_listener(Box::new(listener));
1552
1553 let client = self.market_streamer_client.as_ref().ok_or_else(|| {
1555 AppError::WebSocketError("market streamer client not initialized".to_string())
1556 })?;
1557
1558 {
1559 let mut client = client.lock().await;
1560 client
1561 .connection_options
1562 .set_forced_transport(Some(Transport::WsStreaming));
1563 LightstreamerClient::subscribe(client.subscription_sender.clone(), subscription)
1564 .await?;
1565 }
1566
1567 let (account_tx, account_rx) = mpsc::unbounded_channel();
1569 let handle = tokio::spawn(async move {
1570 let mut receiver = item_receiver;
1571 while let Some(item_update) = receiver.recv().await {
1572 let account_data = crate::presentation::account::AccountData::from(&item_update);
1573 if account_tx.send(account_data.fields).is_err() {
1574 tracing::debug!("Account channel receiver dropped");
1575 break;
1576 }
1577 }
1578 });
1579 self.converter_tasks.push(handle);
1581
1582 info!("Account subscription created for account: {}", account_id);
1583 Ok(account_rx)
1584 }
1585
1586 pub async fn price_subscribe(
1617 &mut self,
1618 epics: Vec<String>,
1619 fields: HashSet<StreamingPriceField>,
1620 ) -> Result<mpsc::UnboundedReceiver<PriceData>, AppError> {
1621 self.has_price_stream_subs = true;
1623
1624 let fields = get_streaming_price_fields(&fields);
1625 let account_id = self.account_id.clone();
1626 let price_epics: Vec<String> = epics
1627 .iter()
1628 .map(|epic| format!("PRICE:{account_id}:{epic}"))
1629 .collect();
1630
1631 tracing::debug!("Pricing subscribe items: {:?}", price_epics);
1633 tracing::debug!("Pricing subscribe fields: {:?}", fields);
1634
1635 let mut subscription =
1636 Subscription::new(SubscriptionMode::Merge, Some(price_epics), Some(fields))?;
1637
1638 let pricing_adapter =
1640 std::env::var("IG_PRICING_ADAPTER").unwrap_or_else(|_| "Pricing".to_string());
1641 tracing::debug!("Using Pricing data adapter: {}", pricing_adapter);
1642 subscription.set_data_adapter(Some(pricing_adapter))?;
1643 subscription.set_requested_snapshot(Some(Snapshot::Yes))?;
1644
1645 let (listener, item_receiver) = ChannelSubscriptionListener::create_channel();
1647 subscription.add_listener(Box::new(listener));
1648
1649 let client = self.price_streamer_client.as_ref().ok_or_else(|| {
1651 AppError::WebSocketError("price streamer client not initialized".to_string())
1652 })?;
1653
1654 {
1655 let mut client = client.lock().await;
1656 client
1657 .connection_options
1658 .set_forced_transport(Some(Transport::WsStreaming));
1659 LightstreamerClient::subscribe(client.subscription_sender.clone(), subscription)
1660 .await?;
1661 }
1662
1663 let (price_tx, price_rx) = mpsc::unbounded_channel();
1665 let handle = tokio::spawn(async move {
1666 let mut receiver = item_receiver;
1667 while let Some(item_update) = receiver.recv().await {
1668 let price_data = PriceData::from(&item_update);
1669 if price_tx.send(price_data).is_err() {
1670 tracing::debug!("Price channel receiver dropped");
1671 break;
1672 }
1673 }
1674 });
1675 self.converter_tasks.push(handle);
1677
1678 info!(
1679 "Price subscription created for {} instruments (account: {})",
1680 epics.len(),
1681 account_id
1682 );
1683 Ok(price_rx)
1684 }
1685
1686 pub async fn chart_subscribe(
1719 &mut self,
1720 epics: Vec<String>,
1721 scale: ChartScale,
1722 fields: HashSet<StreamingChartField>,
1723 ) -> Result<mpsc::UnboundedReceiver<ChartData>, AppError> {
1724 self.has_market_stream_subs = true;
1726
1727 let fields = get_streaming_chart_fields(&fields);
1728
1729 let chart_items: Vec<String> = epics
1730 .iter()
1731 .map(|epic| format!("CHART:{epic}:{scale}",))
1732 .collect();
1733
1734 let mode = if matches!(scale, ChartScale::Tick) {
1736 SubscriptionMode::Distinct
1737 } else {
1738 SubscriptionMode::Merge
1739 };
1740
1741 let mut subscription = Subscription::new(mode, Some(chart_items), Some(fields))?;
1742
1743 subscription.set_data_adapter(None)?;
1744 subscription.set_requested_snapshot(Some(Snapshot::Yes))?;
1745
1746 let (listener, item_receiver) = ChannelSubscriptionListener::create_channel();
1748 subscription.add_listener(Box::new(listener));
1749
1750 let client = self.market_streamer_client.as_ref().ok_or_else(|| {
1752 AppError::WebSocketError("market streamer client not initialized".to_string())
1753 })?;
1754
1755 {
1756 let mut client = client.lock().await;
1757 client
1758 .connection_options
1759 .set_forced_transport(Some(Transport::WsStreaming));
1760 LightstreamerClient::subscribe(client.subscription_sender.clone(), subscription)
1761 .await?;
1762 }
1763
1764 let (chart_tx, chart_rx) = mpsc::unbounded_channel();
1766 let handle = tokio::spawn(async move {
1767 let mut receiver = item_receiver;
1768 while let Some(item_update) = receiver.recv().await {
1769 let chart_data = ChartData::from(&item_update);
1770 if chart_tx.send(chart_data).is_err() {
1771 tracing::debug!("Chart channel receiver dropped");
1772 break;
1773 }
1774 }
1775 });
1776 self.converter_tasks.push(handle);
1778
1779 info!(
1780 "Chart subscription created for {} instruments (scale: {})",
1781 epics.len(),
1782 scale
1783 );
1784
1785 Ok(chart_rx)
1786 }
1787
1788 pub async fn connect(&mut self, shutdown_signal: Option<Arc<Notify>>) -> Result<(), AppError> {
1805 let signal = if let Some(sig) = shutdown_signal {
1807 sig
1808 } else {
1809 let sig = Arc::new(Notify::new());
1810 setup_signal_hook(Arc::clone(&sig)).await;
1811 sig
1812 };
1813
1814 let mut tasks = Vec::new();
1815 let mut connection_signals: Vec<Arc<Notify>> = Vec::new();
1820
1821 if self.has_market_stream_subs {
1823 if let Some(client) = self.market_streamer_client.as_ref() {
1824 let client = Arc::clone(client);
1825 let conn_signal = Arc::new(Notify::new());
1826 connection_signals.push(Arc::clone(&conn_signal));
1827 let task = tokio::spawn(async move {
1828 Self::connect_client(client, conn_signal, "Market").await
1829 });
1830 tasks.push(task);
1831 }
1832 } else {
1833 info!("Skipping Market streamer connection: no active subscriptions");
1834 }
1835
1836 if self.has_price_stream_subs {
1838 if let Some(client) = self.price_streamer_client.as_ref() {
1839 let client = Arc::clone(client);
1840 let conn_signal = Arc::new(Notify::new());
1841 connection_signals.push(Arc::clone(&conn_signal));
1842 let task = tokio::spawn(async move {
1843 Self::connect_client(client, conn_signal, "Price").await
1844 });
1845 tasks.push(task);
1846 }
1847 } else {
1848 info!("Skipping Price streamer connection: no active subscriptions");
1849 }
1850
1851 if tasks.is_empty() {
1852 warn!("No streaming clients selected for connection (no active subscriptions)");
1853 return Ok(());
1854 }
1855
1856 info!("Connecting {} streaming client(s)...", tasks.len());
1857
1858 let fanout = spawn_shutdown_fanout(Arc::clone(&signal), connection_signals);
1862
1863 let results = futures::future::join_all(tasks).await;
1865
1866 fanout.abort();
1869
1870 let mut has_error = false;
1872 for (idx, result) in results.iter().enumerate() {
1873 match result {
1874 Ok(Ok(_)) => {
1875 debug!("Streaming client {} completed successfully", idx);
1876 }
1877 Ok(Err(e)) => {
1878 error!("Streaming client {} failed: {:?}", idx, e);
1879 has_error = true;
1880 }
1881 Err(e) => {
1882 error!("Streaming client {} task panicked: {:?}", idx, e);
1883 has_error = true;
1884 }
1885 }
1886 }
1887
1888 if has_error {
1889 return Err(AppError::WebSocketError(
1890 "one or more streaming connections failed".to_string(),
1891 ));
1892 }
1893
1894 info!("All streaming connections closed gracefully");
1895 Ok(())
1896 }
1897
1898 async fn connect_client(
1900 client: Arc<Mutex<LightstreamerClient>>,
1901 signal: Arc<Notify>,
1902 client_type: &str,
1903 ) -> Result<(), AppError> {
1904 let mut retry_interval_millis: u64 = 0;
1905 let mut retry_counter: u64 = 0;
1906
1907 while retry_counter < MAX_CONNECTION_ATTEMPTS {
1908 let connect_result = {
1909 let mut client = client.lock().await;
1910 client.connect_direct(Arc::clone(&signal)).await
1911 };
1912
1913 match connect_result {
1914 Ok(()) => {
1915 info!("{} streamer connected successfully", client_type);
1916 break;
1917 }
1918 Err(e) => {
1919 if is_graceful_close(&e) {
1924 info!(
1925 "{} streamer closed gracefully: no active subscriptions (server reason: {})",
1926 client_type, GRACEFUL_CLOSE_MARKER
1927 );
1928 return Ok(());
1929 }
1930
1931 let error_msg = e.to_string();
1935 error!("{} streamer connection failed: {}", client_type, error_msg);
1936
1937 if retry_counter < MAX_CONNECTION_ATTEMPTS - 1 {
1938 sleep(Duration::from_millis(retry_interval_millis)).await;
1939 retry_interval_millis =
1940 (retry_interval_millis + (200 * retry_counter)).min(5000);
1941 retry_counter += 1;
1942 warn!(
1943 "{} streamer retrying (attempt {}/{}) in {:.2} seconds...",
1944 client_type,
1945 retry_counter + 1,
1946 MAX_CONNECTION_ATTEMPTS,
1947 retry_interval_millis as f64 / 1000.0
1948 );
1949 } else {
1950 retry_counter += 1;
1951 }
1952 }
1953 }
1954 }
1955
1956 if retry_counter >= MAX_CONNECTION_ATTEMPTS {
1957 error!(
1958 "{} streamer failed after {} attempts",
1959 client_type, MAX_CONNECTION_ATTEMPTS
1960 );
1961 return Err(AppError::WebSocketError(format!(
1962 "{} streamer: maximum connection attempts ({}) exceeded",
1963 client_type, MAX_CONNECTION_ATTEMPTS
1964 )));
1965 }
1966
1967 info!("{} streamer connection closed gracefully", client_type);
1968 Ok(())
1969 }
1970
1971 pub async fn disconnect(&mut self) -> Result<(), AppError> {
1985 let mut disconnected = 0;
1986
1987 if let Some(client) = self.market_streamer_client.as_ref() {
1988 let mut client = client.lock().await;
1989 client.disconnect().await;
1990 info!("Market streamer disconnected");
1991 disconnected += 1;
1992 }
1993
1994 if let Some(client) = self.price_streamer_client.as_ref() {
1995 let mut client = client.lock().await;
1996 client.disconnect().await;
1997 info!("Price streamer disconnected");
1998 disconnected += 1;
1999 }
2000
2001 let converter_count = self.converter_tasks.len();
2004 abort_and_drain_tasks(&mut self.converter_tasks).await;
2005 if converter_count > 0 {
2006 debug!("Aborted {} converter task(s)", converter_count);
2007 }
2008
2009 info!("Disconnected {} streaming client(s)", disconnected);
2010 Ok(())
2011 }
2012}
2013
2014impl Drop for StreamerClient {
2015 fn drop(&mut self) {
2019 for handle in self.converter_tasks.drain(..) {
2020 handle.abort();
2021 }
2022 }
2023}
2024
2025#[cfg(test)]
2026mod tests {
2027 use super::is_transient_confirmation_error;
2028 use crate::error::AppError;
2029 use reqwest::StatusCode;
2030
2031 #[test]
2032 fn test_confirmation_error_rate_limit_is_transient() {
2033 assert!(is_transient_confirmation_error(
2034 &AppError::RateLimitExceeded
2035 ));
2036 }
2037
2038 #[test]
2039 fn test_confirmation_error_not_found_is_transient() {
2040 assert!(is_transient_confirmation_error(&AppError::NotFound));
2042 assert!(is_transient_confirmation_error(&AppError::Unexpected(
2043 StatusCode::NOT_FOUND
2044 )));
2045 }
2046
2047 #[test]
2048 fn test_confirmation_error_server_error_is_transient() {
2049 assert!(is_transient_confirmation_error(&AppError::Unexpected(
2050 StatusCode::INTERNAL_SERVER_ERROR
2051 )));
2052 assert!(is_transient_confirmation_error(&AppError::Unexpected(
2053 StatusCode::BAD_GATEWAY
2054 )));
2055 }
2056
2057 #[test]
2058 fn test_confirmation_error_invalid_input_is_permanent() {
2059 assert!(!is_transient_confirmation_error(&AppError::InvalidInput(
2060 "bad".to_string()
2061 )));
2062 }
2063
2064 #[test]
2065 fn test_confirmation_error_auth_and_deser_are_permanent() {
2066 assert!(!is_transient_confirmation_error(&AppError::Unauthorized));
2067 assert!(!is_transient_confirmation_error(
2068 &AppError::OAuthTokenExpired
2069 ));
2070 assert!(!is_transient_confirmation_error(
2071 &AppError::Deserialization("bad".to_string())
2072 ));
2073 assert!(!is_transient_confirmation_error(&AppError::Unexpected(
2074 StatusCode::BAD_REQUEST
2075 )));
2076 }
2077
2078 use super::{
2079 GRACEFUL_CLOSE_MARKER, abort_and_drain_tasks, is_graceful_close, spawn_shutdown_fanout,
2080 };
2081 use lightstreamer_rs::utils::LightstreamerError;
2082 use std::sync::Arc;
2083 use std::time::Duration;
2084 use tokio::sync::Notify;
2085 use tokio::task::JoinHandle;
2086
2087 #[test]
2090 fn test_is_graceful_close_connection_variant_with_marker_is_true() {
2091 let err = LightstreamerError::Connection(format!(
2094 "connection error from server: conerr,-2,{GRACEFUL_CLOSE_MARKER}"
2095 ));
2096 assert!(is_graceful_close(&err));
2097 }
2098
2099 #[test]
2100 fn test_is_graceful_close_protocol_and_invalid_state_with_marker_is_true() {
2101 let protocol = LightstreamerError::Protocol(format!("closing: {GRACEFUL_CLOSE_MARKER}"));
2102 let invalid_state =
2103 LightstreamerError::InvalidState(format!("state: {GRACEFUL_CLOSE_MARKER}"));
2104 assert!(is_graceful_close(&protocol));
2105 assert!(is_graceful_close(&invalid_state));
2106 }
2107
2108 #[test]
2109 fn test_is_graceful_close_connection_variant_without_marker_is_false() {
2110 let err = LightstreamerError::Connection("no message received within 5000 ms".to_string());
2111 assert!(!is_graceful_close(&err));
2112 }
2113
2114 #[test]
2115 fn test_is_graceful_close_other_variant_with_marker_is_false() {
2116 let err = LightstreamerError::Timeout(GRACEFUL_CLOSE_MARKER.to_string());
2119 assert!(!is_graceful_close(&err));
2120 }
2121
2122 #[tokio::test]
2125 async fn test_shutdown_fanout_wakes_all_connection_waiters() {
2126 let source = Arc::new(Notify::new());
2129 let market_signal = Arc::new(Notify::new());
2130 let price_signal = Arc::new(Notify::new());
2131
2132 let fanout = spawn_shutdown_fanout(
2133 Arc::clone(&source),
2134 vec![Arc::clone(&market_signal), Arc::clone(&price_signal)],
2135 );
2136
2137 let market_waiter = tokio::spawn(async move { market_signal.notified().await });
2138 let price_waiter = tokio::spawn(async move { price_signal.notified().await });
2139
2140 source.notify_one();
2145
2146 assert!(
2147 tokio::time::timeout(Duration::from_secs(1), market_waiter)
2148 .await
2149 .is_ok(),
2150 "market connection did not observe the shutdown signal"
2151 );
2152 assert!(
2153 tokio::time::timeout(Duration::from_secs(1), price_waiter)
2154 .await
2155 .is_ok(),
2156 "price connection did not observe the shutdown signal"
2157 );
2158
2159 fanout.abort();
2160 }
2161
2162 #[tokio::test]
2165 async fn test_abort_and_drain_tasks_stops_and_clears() {
2166 let mut tasks: Vec<JoinHandle<()>> = Vec::new();
2167 for _ in 0..3 {
2168 tasks.push(tokio::spawn(async { std::future::pending::<()>().await }));
2170 }
2171 assert!(tasks.iter().all(|h| !h.is_finished()));
2172
2173 abort_and_drain_tasks(&mut tasks).await;
2174
2175 assert!(tasks.is_empty());
2178 }
2179
2180 #[tokio::test]
2181 async fn test_abort_makes_pending_task_finish() {
2182 let handle: JoinHandle<()> = tokio::spawn(async { std::future::pending::<()>().await });
2183 assert!(!handle.is_finished());
2184 handle.abort();
2185 let join_result = handle.await;
2186 assert!(
2187 join_result.is_err(),
2188 "aborted task should yield a cancellation JoinError"
2189 );
2190 }
2191}