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);
}
}