249 lines
10 KiB
C#
249 lines
10 KiB
C#
using System;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Timers;
|
|
using FinlyticCore.Dtos.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);
|
|
|
|
/// <summary>
|
|
/// Fetches stock details (company description, events, earnings, analyst ratings) for a specific ISIN.
|
|
/// </summary>
|
|
/// <param name="isin">The ISIN of the stock.</param>
|
|
/// <param name="cancellationToken">A cancellation token.</param>
|
|
/// <returns>The stock details response, or null if failed.</returns>
|
|
Task<TradeRepublicStockDetailsResponse?> GetStockDetailsAsync(string isin, CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Fetches derivative products (KnockOuts, Warrants, Factor Certificates) for an underlying ISIN.
|
|
/// </summary>
|
|
/// <param name="request">The derivative query parameters.</param>
|
|
/// <param name="cancellationToken">A cancellation token.</param>
|
|
/// <returns>The derivatives response, or null if failed.</returns>
|
|
Task<TradeRepublicDerivativesResponse?> GetDerivativesAsync(TradeRepublicDerivativesRequest request, CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<TradeRepublicStockDetailsResponse?> GetStockDetailsAsync(string isin, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
await EnsureConnectedAsync();
|
|
var req = new TradeRepublicStockDetailsRequest(Id: isin);
|
|
return await _client.SendRequestAsync<TradeRepublicStockDetailsResponse, TradeRepublicStockDetailsRequest>(req, cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "[{Channel}] Error while fetching stock details for ISIN {Isin}", "TradeRepublicChannel", isin);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<TradeRepublicDerivativesResponse?> GetDerivativesAsync(TradeRepublicDerivativesRequest request, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
await EnsureConnectedAsync();
|
|
return await _client.SendRequestAsync<TradeRepublicDerivativesResponse, TradeRepublicDerivativesRequest>(request, cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "[{Channel}] Error while fetching derivatives for underlying {Underlying}", "TradeRepublicChannel", request.Underlying);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|