This commit is contained in:
2026-07-19 12:13:14 +02:00
commit 6337e63a77
46 changed files with 4411 additions and 0 deletions
@@ -0,0 +1,240 @@
using System.Timers;
using FinlyticAssets.Models;
using FinlyticAssets.Models.DataToObject.TradeRepublic;
using FinlyticAssets.Util;
using FinlyticCore.Models.Assets;
namespace FinlyticAssets.Services;
public interface ITradeRepublicService
{
/// <summary>
/// Holt die Anzahl der Assets pro Typ für die Paginierung des Initial-Scans.
/// </summary>
public Task<AssetsCount> GetAssetsCount(CancellationToken cancellationToken = default);
/// <summary>
/// Holt eine spezifische Seite an Assets für den Initial-Scan.
/// </summary>
public Task<TradeRepublicAssetResponse?> GetAssets(AssetType type, int page, int pageSize,
CancellationToken cancellationToken = default);
/// <summary>
/// Holt die aktuellen Stammdaten für eine spezifische ISIN (Gezieltes Update).
/// Gibt null zurück, wenn das Asset bei TR nicht mehr existiert.
/// </summary>
public Task<TradeRepublicAssetResponse?> GetAsset(string isin, CancellationToken cancellationToken = default);
}
/// <summary>
/// Provides a managed service to interact with the Trade Republic API via WebSockets,
/// featuring an automatic inactivity timeout to mimic human behavior.
/// </summary>
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);
/// <summary>
/// Initializes a new instance of the <see cref="TradeRepublicService"/> class.
/// </summary>
/// <param name="client">The underlying managed WebSocket client.</param>
/// <param name="logger">The logger instance.</param>
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;
}
/// <summary>
/// Ensures that the Trade Republic WebSocket client is connected, initiating a new connection if necessary.
/// Also handles resetting the inactivity timer.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
private async Task EnsureConnectedAsync()
{
await _lock.WaitAsync();
try
{
_inactivityTimer.Stop();
if (!_client.IsConnected)
{
_logger.LogInformation("Trade Republic API is not connected. Establishing automated connection...");
// Nutzt den boolschen Rückgabewert von InitAsync
bool connected = await _client.InitAsync();
if (connected)
{
_logger.LogInformation("Successfully connected to Trade Republic API.");
}
else
{
_logger.LogWarning("Trade Republic API connection initialization failed (InitAsync returned false).");
}
}
_inactivityTimer.Start();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to establish a connection to the Trade Republic API.");
throw;
}
finally
{
_lock.Release();
}
}
/// <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>
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 TradeRepublicFilter("type", type.ToString().ToLowerInvariant()),
new TradeRepublicFilter("jurisdiction", "DE"),
]
};
var request = new TradeRepublicSearchRequest(Data: reqData);
var response =
await _client.SendRequestAsync<TradeRepublicAssetResponse, TradeRepublicSearchRequest>(request);
var count = response?.ResultCount ?? 0;
counts.SetCountOfType(type, count);
await Task.Delay(TimeSpan.FromMilliseconds(320), cancellationToken);
}
return counts;
}
/// <summary>
/// Retrieves a paginated chunk of assets filtered by a specific type.
/// </summary>
/// <param name="type">The type of assets to retrieve (e.g., Stock, Etf).</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>
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 TradeRepublicFilter("type", type.ToString().ToLowerInvariant()),
new TradeRepublicFilter("jurisdiction", "DE"),
]
};
var request = new TradeRepublicSearchRequest(Data: reqData);
return await _client.SendRequestAsync<TradeRepublicAssetResponse, TradeRepublicSearchRequest>(request);
}
/// <summary>
/// Retrieves the static metadata for a single specific asset via its ISIN.
/// </summary>
/// <param name="isin">The International Securities Identification Number of the target asset.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A <see cref="TradeRepublicAssetResponse"/> containing instrument details, or null if the asset is not found.</returns>
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 TradeRepublicFilter("jurisdiction", "DE"),
]
};
var request = new TradeRepublicSearchRequest(Data: reqData);
return await _client.SendRequestAsync<TradeRepublicAssetResponse, TradeRepublicSearchRequest>(request);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while fetching asset metadata for ISIN {Isin}", isin);
return null;
}
}
/// <summary>
/// Event handler executed when the inactivity timer expires.
/// Gracefully disconnects the WebSocket client.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">An EventData object that contains the event data.</param>
private async void OnInactivityTimeout(object? sender, ElapsedEventArgs e)
{
try
{
await _lock.WaitAsync();
if (!_client.IsConnected) return;
_logger.LogInformation("No active requests detected for 5 minutes. Automatically disconnecting WebSocket.");
await _client.DisconnectAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during automatic inactivity disconnect procedure.");
//ignore
}
finally
{
_lock.Release();
}
}
/// <summary>
/// Disposes the underlying timer and synchronization primitives.
/// </summary>
public void Dispose()
{
_inactivityTimer.Dispose();
_lock.Dispose();
GC.SuppressFinalize(this);
}
}