using System; using System.Threading; using System.Threading.Tasks; using System.Timers; using FinlyticCore.Dtos.TradeRepublic; using FinlyticCore.Models.Assets; using FinlyticCore.Models.Settings; namespace FinlyticCore.Services.TradeRepublic; /// /// Service for interacting with the Trade Republic API. /// public interface ITradeRepublicService { /// /// Fetches asset metadata from Trade Republic by ISIN. /// Task GetAsset(string isin, CancellationToken cancellationToken = default); /// /// Retrieves the total count of available assets grouped by their types. /// Task GetAssetsCount(CancellationToken cancellationToken = default); /// /// Retrieves a paginated chunk of assets filtered by a specific type. /// Task GetAssets(AssetType type, int page, int pageSize, CancellationToken cancellationToken = default); /// /// Subscribes to the real-time ticker stream for a specific ISIN. /// Task SubscribeRealtimeTickerAsync(string isin, Action onTick, CancellationToken cancellationToken = default); /// /// Unsubscribes from a real-time ticker stream. /// Task UnsubscribeRealtimeTickerAsync(int subId); /// /// Fetches stock details (company description, events, earnings, analyst ratings) for a specific ISIN. /// Task GetStockDetailsAsync(string isin, CancellationToken cancellationToken = default); /// /// Fetches derivative products (KnockOuts, Warrants, Factor Certificates) for an underlying ISIN. /// Task GetDerivativesAsync(TradeRepublicDerivativesRequest request, CancellationToken cancellationToken = default); } public class TradeRepublicService : ITradeRepublicService, IDisposable { private readonly TradeRepublicClient _client; private readonly IFinlyticLogger _finlyticLogger; private readonly System.Timers.Timer _inactivityTimer; private readonly SemaphoreSlim _lock = new(1, 1); public TradeRepublicService(TradeRepublicClient client, IFinlyticLogger finlyticLogger) { _client = client; _finlyticLogger = finlyticLogger; _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) { await _finlyticLogger.LogInfoAsync(CoreSettingKeys.TradeRepublicChannel, "[TradeRepublicService] Connecting to Trade Republic API WebSocket..."); bool connected = await _client.InitAsync(); if (!connected) { await _finlyticLogger.LogWarningAsync(CoreSettingKeys.TradeRepublicChannel, "[TradeRepublicService] Trade Republic WebSocket connection failed or timed out."); throw new InvalidOperationException("Trade Republic WebSocket is not connected."); } await _finlyticLogger.LogInfoAsync(CoreSettingKeys.TradeRepublicChannel, "[TradeRepublicService] Successfully connected to Trade Republic API."); } _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) { await _finlyticLogger.LogErrorAsync(CoreSettingKeys.TradeRepublicChannel, ex, "[TradeRepublicService] Error while fetching asset metadata for ISIN {Isin}", 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) { await _finlyticLogger.LogErrorAsync(CoreSettingKeys.TradeRepublicChannel, ex, "[TradeRepublicService] Error while fetching stock details for ISIN {Isin}", isin); return null; } } /// public async Task GetDerivativesAsync(TradeRepublicDerivativesRequest request, CancellationToken cancellationToken = default) { try { await EnsureConnectedAsync(); return await _client.SendRequestAsync(request, cancellationToken); } catch (Exception ex) { await _finlyticLogger.LogErrorAsync(CoreSettingKeys.TradeRepublicChannel, ex, "[TradeRepublicService] Error while fetching derivatives for underlying {Underlying}", request.Underlying); return null; } } private async void OnInactivityTimeout(object? sender, ElapsedEventArgs e) { try { await _lock.WaitAsync(); if (!_client.IsConnected) return; await _finlyticLogger.LogInfoAsync(CoreSettingKeys.TradeRepublicChannel, "[TradeRepublicService] Inactivity timer expired. Auto-disconnecting Trade Republic WebSocket."); await _client.DisconnectAsync(); } catch { } finally { _lock.Release(); } } public void Dispose() { _inactivityTimer.Dispose(); _lock.Dispose(); GC.SuppressFinalize(this); } }