using System; using System.Threading; using System.Threading.Tasks; using System.Timers; using FinlyticCore.Dtos.TradeRepublic; using FinlyticCore.Models.Assets; using Microsoft.Extensions.Logging; namespace FinlyticCore.Services.TradeRepublic; /// /// Service for interacting with the Trade Republic API. /// public interface ITradeRepublicService { /// /// Fetches asset metadata from Trade Republic by ISIN. /// /// The ISIN to search for. /// A cancellation token. /// The Trade Republic search response, or null if not found/failed. Task GetAsset(string isin, CancellationToken cancellationToken = default); /// /// Retrieves the total count of available assets grouped by their types. /// /// A token to monitor for cancellation requests. /// An object containing the metrics. Task GetAssetsCount(CancellationToken cancellationToken = default); /// /// Retrieves a paginated chunk of assets filtered by a specific type. /// /// The type of assets to retrieve. /// 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. Task GetAssets(AssetType type, int page, int pageSize, CancellationToken cancellationToken = default); /// /// Subscribes to the real-time ticker stream for a specific ISIN. /// /// The ISIN. /// The callback action when a tick is received. /// A cancellation token. /// The subscription ID, or null if failed. Task SubscribeRealtimeTickerAsync(string isin, Action onTick, CancellationToken cancellationToken = default); /// /// Unsubscribes from a real-time ticker stream. /// /// The subscription ID to unsubscribe. /// A task representing the async operation. Task UnsubscribeRealtimeTickerAsync(int subId); /// /// Fetches stock details (company description, events, earnings, analyst ratings) for a specific ISIN. /// /// The ISIN of the stock. /// A cancellation token. /// The stock details response, or null if failed. Task GetStockDetailsAsync(string isin, CancellationToken cancellationToken = default); /// /// Fetches derivative products (KnockOuts, Warrants, Factor Certificates) for an underlying ISIN. /// /// The derivative query parameters. /// A cancellation token. /// The derivatives response, or null if failed. Task GetDerivativesAsync(TradeRepublicDerivativesRequest request, CancellationToken cancellationToken = default); } 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); 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; } private async Task EnsureConnectedAsync() { await _lock.WaitAsync(); try { _inactivityTimer.Stop(); if (!_client.IsConnected) { _logger.LogInformation("[{Channel}] Connecting to Trade Republic API WebSocket...", "TradeRepublicChannel"); bool connected = await _client.InitAsync(); if (!connected) { _logger.LogWarning("[{Channel}] Trade Republic WebSocket connection failed or timed out.", "TradeRepublicChannel"); throw new InvalidOperationException("Trade Republic WebSocket is not connected."); } _logger.LogInformation("[{Channel}] Successfully connected to Trade Republic API.", "TradeRepublicChannel"); } _inactivityTimer.Start(); } finally { _lock.Release(); } } /// public async Task GetAsset(string isin, CancellationToken cancellationToken = default) { try { await EnsureConnectedAsync(); var reqData = new TradeRepublicSearchData { Query = isin, Page = 1, PageSize = 1, Filter = new[] { new TradeRepublicFilter("jurisdiction", "DE") } }; var request = new TradeRepublicSearchRequest(Data: reqData); return await _client.SendRequestAsync(request, cancellationToken); } catch (Exception ex) { _logger.LogError(ex, "[{Channel}] Error while fetching asset metadata for ISIN {Isin}", "TradeRepublicChannel", isin); return null; } } /// 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[] { new TradeRepublicFilter("type", type.ToString().ToLowerInvariant()), new TradeRepublicFilter("jurisdiction", "DE") } }; var request = new TradeRepublicSearchRequest(Data: reqData); var response = await _client.SendRequestAsync(request, cancellationToken); var count = response?.ResultCount ?? 0; counts.SetCountOfType(type, count); await Task.Delay(TimeSpan.FromMilliseconds(320), cancellationToken); } return counts; } /// 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[] { new TradeRepublicFilter("type", type.ToString().ToLowerInvariant()), new TradeRepublicFilter("jurisdiction", "DE") } }; var request = new TradeRepublicSearchRequest(Data: reqData); return await _client.SendRequestAsync(request, cancellationToken); } /// public async Task SubscribeRealtimeTickerAsync(string isin, Action onTick, CancellationToken cancellationToken = default) { await EnsureConnectedAsync(); return await _client.SubscribeTickerAsync(isin, onTick, cancellationToken); } /// public async Task UnsubscribeRealtimeTickerAsync(int subId) { await _client.UnsubscribeTickerAsync(subId); } /// public async Task GetStockDetailsAsync(string isin, CancellationToken cancellationToken = default) { try { await EnsureConnectedAsync(); var req = new TradeRepublicStockDetailsRequest(Id: isin); return await _client.SendRequestAsync(req, cancellationToken); } catch (Exception ex) { _logger.LogError(ex, "[{Channel}] Error while fetching stock details for ISIN {Isin}", "TradeRepublicChannel", isin); return null; } } /// public async Task GetDerivativesAsync(TradeRepublicDerivativesRequest request, CancellationToken cancellationToken = default) { try { await EnsureConnectedAsync(); return await _client.SendRequestAsync(request, cancellationToken); } catch (Exception ex) { _logger.LogError(ex, "[{Channel}] Error while fetching derivatives for underlying {Underlying}", "TradeRepublicChannel", request.Underlying); return null; } } private async void OnInactivityTimeout(object? sender, ElapsedEventArgs e) { try { await _lock.WaitAsync(); if (!_client.IsConnected) return; _logger.LogInformation("[{Channel}] Inactivity timer expired. Auto-disconnecting Trade Republic WebSocket.", "TradeRepublicChannel"); await _client.DisconnectAsync(); } catch { } finally { _lock.Release(); } } public void Dispose() { _inactivityTimer.Dispose(); _lock.Dispose(); GC.SuppressFinalize(this); } }