using System.Timers;
using FinlyticAssets.Models;
using FinlyticAssets.Models.DataToObject.TradeRepublic;
using FinlyticAssets.Util;
using FinlyticCore.Models.Assets;
namespace FinlyticAssets.Services;
public interface ITradeRepublicService
{
///
/// Holt die Anzahl der Assets pro Typ für die Paginierung des Initial-Scans.
///
public Task GetAssetsCount(CancellationToken cancellationToken = default);
///
/// Holt eine spezifische Seite an Assets für den Initial-Scan.
///
public Task GetAssets(AssetType type, int page, int pageSize,
CancellationToken cancellationToken = default);
///
/// Holt die aktuellen Stammdaten für eine spezifische ISIN (Gezieltes Update).
/// Gibt null zurück, wenn das Asset bei TR nicht mehr existiert.
///
public Task GetAsset(string isin, CancellationToken cancellationToken = default);
}
///
/// Provides a managed service to interact with the Trade Republic API via WebSockets,
/// featuring an automatic inactivity timeout to mimic human behavior.
///
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);
///
/// Initializes a new instance of the class.
///
/// The underlying managed WebSocket client.
/// The logger instance.
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;
}
///
/// Ensures that the Trade Republic WebSocket client is connected, initiating a new connection if necessary.
/// Also handles resetting the inactivity timer.
///
/// A task representing the asynchronous operation.
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();
}
}
///
/// Retrieves the total count of available assets grouped by their types.
///
/// A token to monitor for cancellation requests.
/// An object containing the metrics.
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 TradeRepublicFilter("type", type.ToString().ToLowerInvariant()),
new TradeRepublicFilter("jurisdiction", "DE"),
]
};
var request = new TradeRepublicSearchRequest(Data: reqData);
var response =
await _client.SendRequestAsync(request);
var count = response?.ResultCount ?? 0;
counts.SetCountOfType(type, count);
await Task.Delay(TimeSpan.FromMilliseconds(320), cancellationToken);
}
return counts;
}
///
/// Retrieves a paginated chunk of assets filtered by a specific type.
///
/// The type of assets to retrieve (e.g., Stock, Etf).
/// 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.
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 TradeRepublicFilter("type", type.ToString().ToLowerInvariant()),
new TradeRepublicFilter("jurisdiction", "DE"),
]
};
var request = new TradeRepublicSearchRequest(Data: reqData);
return await _client.SendRequestAsync(request);
}
///
/// Retrieves the static metadata for a single specific asset via its ISIN.
///
/// The International Securities Identification Number of the target asset.
/// A token to monitor for cancellation requests.
/// A containing instrument details, or null if the asset is not found.
public async Task 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(request);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while fetching asset metadata for ISIN {Isin}", isin);
return null;
}
}
///
/// Event handler executed when the inactivity timer expires.
/// Gracefully disconnects the WebSocket client.
///
/// The source of the event.
/// An EventData object that contains the event data.
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();
}
}
///
/// Disposes the underlying timer and synchronization primitives.
///
public void Dispose()
{
_inactivityTimer.Dispose();
_lock.Dispose();
GC.SuppressFinalize(this);
}
}