using System.Timers; using FinlyticAssets.Models; using FinlyticAssets.Models.DataToObject.TradeRepublic; using FinlyticAssets.Util; using FinlyticCore.Models.Assets; namespace FinlyticAssets.Services; public interface ITradeRepublicService { /// /// Holt die Anzahl der Assets pro Typ für die Paginierung des Initial-Scans. /// public Task GetAssetsCount(CancellationToken cancellationToken = default); /// /// Holt eine spezifische Seite an Assets für den Initial-Scan. /// public Task GetAssets(AssetType type, int page, int pageSize, CancellationToken cancellationToken = default); /// /// Holt die aktuellen Stammdaten für eine spezifische ISIN (Gezieltes Update). /// Gibt null zurück, wenn das Asset bei TR nicht mehr existiert. /// public Task GetAsset(string isin, CancellationToken cancellationToken = default); } /// /// Provides a managed service to interact with the Trade Republic API via WebSockets, /// featuring an automatic inactivity timeout to mimic human behavior. /// public class TradeRepublicService : ITradeRepublicService, IDisposable { private readonly TradeRepublicClient _client; private readonly ILogger _logger; private readonly System.Timers.Timer _inactivityTimer; private readonly SemaphoreSlim _lock = new(1, 1); /// /// Initializes a new instance of the class. /// /// The underlying managed WebSocket client. /// The logger instance. public TradeRepublicService(TradeRepublicClient client, ILogger logger) { _client = client; _logger = logger; _inactivityTimer = new System.Timers.Timer(TimeSpan.FromSeconds(461).TotalMilliseconds); _inactivityTimer.AutoReset = false; _inactivityTimer.Elapsed += OnInactivityTimeout; } /// /// Ensures that the Trade Republic WebSocket client is connected, initiating a new connection if necessary. /// Also handles resetting the inactivity timer. /// /// A task representing the asynchronous operation. private async Task EnsureConnectedAsync() { await _lock.WaitAsync(); try { _inactivityTimer.Stop(); if (!_client.IsConnected) { _logger.LogInformation("Trade Republic API is not connected. Establishing automated connection..."); // Nutzt den boolschen Rückgabewert von InitAsync bool connected = await _client.InitAsync(); if (connected) { _logger.LogInformation("Successfully connected to Trade Republic API."); } else { _logger.LogWarning("Trade Republic API connection initialization failed (InitAsync returned false)."); } } _inactivityTimer.Start(); } catch (Exception ex) { _logger.LogError(ex, "Failed to establish a connection to the Trade Republic API."); throw; } finally { _lock.Release(); } } /// /// Retrieves the total count of available assets grouped by their types. /// /// A token to monitor for cancellation requests. /// An object containing the metrics. public async Task GetAssetsCount(CancellationToken cancellationToken = default) { await EnsureConnectedAsync(); var counts = new AssetsCount(); foreach (var type in Enum.GetValues()) { var reqData = new TradeRepublicSearchData() { Query = "", Page = 1, PageSize = 1, Filter = [ new TradeRepublicFilter("type", type.ToString().ToLowerInvariant()), new TradeRepublicFilter("jurisdiction", "DE"), ] }; var request = new TradeRepublicSearchRequest(Data: reqData); var response = await _client.SendRequestAsync(request); var count = response?.ResultCount ?? 0; counts.SetCountOfType(type, count); await Task.Delay(TimeSpan.FromMilliseconds(320), cancellationToken); } return counts; } /// /// Retrieves a paginated chunk of assets filtered by a specific type. /// /// The type of assets to retrieve (e.g., Stock, Etf). /// The zero-based page index. /// The number of elements per page. /// A token to monitor for cancellation requests. /// A containing the elements, or null if the request fails. public async Task GetAssets(AssetType type, int page, int pageSize, CancellationToken cancellationToken = default) { await EnsureConnectedAsync(); var reqData = new TradeRepublicSearchData() { Query = "", Page = page, PageSize = pageSize, Filter = [ new TradeRepublicFilter("type", type.ToString().ToLowerInvariant()), new TradeRepublicFilter("jurisdiction", "DE"), ] }; var request = new TradeRepublicSearchRequest(Data: reqData); return await _client.SendRequestAsync(request); } /// /// Retrieves the static metadata for a single specific asset via its ISIN. /// /// The International Securities Identification Number of the target asset. /// A token to monitor for cancellation requests. /// A containing instrument details, or null if the asset is not found. public async Task GetAsset(string isin, CancellationToken cancellationToken = default) { try { await EnsureConnectedAsync(); var reqData = new TradeRepublicSearchData() { Query = isin, Page = 1, PageSize = 1, Filter = [ new TradeRepublicFilter("jurisdiction", "DE"), ] }; var request = new TradeRepublicSearchRequest(Data: reqData); return await _client.SendRequestAsync(request); } catch (Exception ex) { _logger.LogError(ex, "Error while fetching asset metadata for ISIN {Isin}", isin); return null; } } /// /// Event handler executed when the inactivity timer expires. /// Gracefully disconnects the WebSocket client. /// /// The source of the event. /// An EventData object that contains the event data. private async void OnInactivityTimeout(object? sender, ElapsedEventArgs e) { try { await _lock.WaitAsync(); if (!_client.IsConnected) return; _logger.LogInformation("No active requests detected for 5 minutes. Automatically disconnecting WebSocket."); await _client.DisconnectAsync(); } catch (Exception ex) { _logger.LogError(ex, "Error during automatic inactivity disconnect procedure."); //ignore } finally { _lock.Release(); } } /// /// Disposes the underlying timer and synchronization primitives. /// public void Dispose() { _inactivityTimer.Dispose(); _lock.Dispose(); GC.SuppressFinalize(this); } }