Files
Finlytic/FinlyticCore/Services/TradeRepublic/TradeRepublicService.cs
T

202 lines
7.9 KiB
C#

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;
/// <summary>
/// Service for interacting with the Trade Republic API.
/// </summary>
public interface ITradeRepublicService
{
/// <summary>
/// Fetches asset metadata from Trade Republic by ISIN.
/// </summary>
/// <param name="isin">The ISIN to search for.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The Trade Republic search response, or null if not found/failed.</returns>
Task<TradeRepublicAssetResponse?> GetAsset(string isin, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the total count of available assets grouped by their types.
/// </summary>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>An <see cref="AssetsCount"/> object containing the metrics.</returns>
Task<AssetsCount> GetAssetsCount(CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a paginated chunk of assets filtered by a specific type.
/// </summary>
/// <param name="type">The type of assets to retrieve.</param>
/// <param name="page">The zero-based page index.</param>
/// <param name="pageSize">The number of elements per page.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A <see cref="TradeRepublicAssetResponse"/> containing the elements, or null if the request fails.</returns>
Task<TradeRepublicAssetResponse?> GetAssets(AssetType type, int page, int pageSize, CancellationToken cancellationToken = default);
/// <summary>
/// Subscribes to the real-time ticker stream for a specific ISIN.
/// </summary>
/// <param name="isin">The ISIN.</param>
/// <param name="onTick">The callback action when a tick is received.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The subscription ID, or null if failed.</returns>
Task<int?> SubscribeRealtimeTickerAsync(string isin, Action<TradeRepublicTickerResponse> onTick, CancellationToken cancellationToken = default);
/// <summary>
/// Unsubscribes from a real-time ticker stream.
/// </summary>
/// <param name="subId">The subscription ID to unsubscribe.</param>
/// <returns>A task representing the async operation.</returns>
Task UnsubscribeRealtimeTickerAsync(int subId);
}
public class TradeRepublicService : ITradeRepublicService, IDisposable
{
private readonly TradeRepublicClient _client;
private readonly ILogger<TradeRepublicService> _logger;
private readonly System.Timers.Timer _inactivityTimer;
private readonly SemaphoreSlim _lock = new(1, 1);
public TradeRepublicService(TradeRepublicClient client, ILogger<TradeRepublicService> 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();
}
}
/// <inheritdoc />
public async Task<TradeRepublicAssetResponse?> 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<TradeRepublicAssetResponse, TradeRepublicSearchRequest>(request, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "[{Channel}] Error while fetching asset metadata for ISIN {Isin}", "TradeRepublicChannel", isin);
return null;
}
}
/// <inheritdoc />
public async Task<AssetsCount> GetAssetsCount(CancellationToken cancellationToken = default)
{
await EnsureConnectedAsync();
var counts = new AssetsCount();
foreach (var type in Enum.GetValues<AssetType>())
{
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<TradeRepublicAssetResponse, TradeRepublicSearchRequest>(request, cancellationToken);
var count = response?.ResultCount ?? 0;
counts.SetCountOfType(type, count);
await Task.Delay(TimeSpan.FromMilliseconds(320), cancellationToken);
}
return counts;
}
/// <inheritdoc />
public async Task<TradeRepublicAssetResponse?> 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<TradeRepublicAssetResponse, TradeRepublicSearchRequest>(request, cancellationToken);
}
/// <inheritdoc />
public async Task<int?> SubscribeRealtimeTickerAsync(string isin, Action<TradeRepublicTickerResponse> onTick, CancellationToken cancellationToken = default)
{
await EnsureConnectedAsync();
return await _client.SubscribeTickerAsync(isin, onTick, cancellationToken);
}
/// <inheritdoc />
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);
}
}