Init
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user