feat(Core): update DTOs and shared models
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user