using System;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using FinlyticCore.Models.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);
}
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);
}
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);
}
}