feat(derivatives): fix Trade Republic derivative pagination streaming and picker modal

This commit is contained in:
2026-08-15 19:30:15 +02:00
parent 7e18257e3e
commit 882d24a316
7 changed files with 1045 additions and 26 deletions
+67 -25
View File
@@ -20,7 +20,7 @@ public interface IAssetsDbService
public Task UpdateAssetImageIdAsync(string isin, string imageId);
public Task<bool> DeleteAssetAsync(string isin);
public Task<List<AssetEntity>> GetDiscoveryAssetsAsync(int limit = 15);
public Task<List<DerivativeEntity>> GetDerivativesByUnderlyingAsync(string underlyingIsin, string optionType = "long", bool forceRefresh = false, CancellationToken cancellationToken = default);
public Task<List<DerivativeEntity>> GetDerivativesByUnderlyingAsync(string underlyingIsin, string optionType = "long", decimal? targetLeverage = null, string? after = null, int? page = null, bool forceRefresh = false, CancellationToken cancellationToken = default);
}
/// <inheritdoc />
@@ -394,38 +394,56 @@ public class AssetsDbService : IAssetsDbService
}
/// <summary>Inherits documentation from interface.</summary>
public async Task<List<DerivativeEntity>> GetDerivativesByUnderlyingAsync(string underlyingIsin, string optionType = "long", bool forceRefresh = false, CancellationToken cancellationToken = default)
public async Task<List<DerivativeEntity>> GetDerivativesByUnderlyingAsync(
string underlyingIsin,
string optionType = "long",
decimal? targetLeverage = null,
string? after = null,
int? page = null,
bool forceRefresh = false,
CancellationToken cancellationToken = default)
{
var targetOptionType = optionType.Equals("short", StringComparison.OrdinalIgnoreCase) ? OptionType.Short : OptionType.Long;
var cutoff = DateTime.UtcNow.AddDays(-7);
string cleanOptionType = optionType.Equals("short", StringComparison.OrdinalIgnoreCase) ? "short" : "long";
const int pageSize = 50;
int pageIndex = Math.Max(0, page ?? 0);
if (!forceRefresh)
{
var cached = await _context.TradeRepublicAssets
.OfType<DerivativeEntity>()
.AsNoTracking()
.Include(a => a.Tags)
.Where(d => d.UnderlyingIsin == underlyingIsin && d.OptionType == targetOptionType && d.LastUpdatedAt >= cutoff)
.ToListAsync(cancellationToken);
decimal levQuery = targetLeverage.HasValue && targetLeverage.Value > 0 ? targetLeverage.Value : 0m;
if (cached.Count > 0)
{
return cached;
}
}
// Trade Republic uses page index (0, 1, 2, 3...) for the 'after' pagination parameter in derivatives
string trAfter = !string.IsNullOrEmpty(after) ? after : (pageIndex > 0 ? pageIndex.ToString() : "0");
_logger.LogInformation("[{Channel}] Fetching derivatives for {Isin} (OptionType: {Option}, Leverage: {Lev}, Page: {Page}, TR-After: {After})",
"AssetsChannel", underlyingIsin, cleanOptionType, levQuery, pageIndex, trAfter);
var trReq = new TradeRepublicDerivativesRequest(
Underlying: underlyingIsin,
OptionType: cleanOptionType,
ProductCategory: "knockOutProduct",
Leverage: levQuery,
SortBy: "leverage",
SortDirection: "asc",
PageSize: pageSize,
After: trAfter);
var trReq = new TradeRepublicDerivativesRequest(Underlying: underlyingIsin, OptionType: optionType, ProductCategory: "knockOutProduct", PageSize: 50, After: "0");
var trResponse = await _tradeRepublicService.GetDerivativesAsync(trReq, cancellationToken);
if (trResponse?.Results != null && trResponse.Results.Count > 0)
var fetchedItems = trResponse?.Results ?? new List<TradeRepublicDerivativeItemDto>();
_logger.LogInformation("[{Channel}] TR returned {Count} derivatives for {Isin} (Cursors.After: {NextAfter})",
"AssetsChannel", fetchedItems.Count, underlyingIsin, trResponse?.Cursors?.After ?? "null");
if (fetchedItems.Count > 0)
{
var now = DateTime.UtcNow;
var isins = trResponse.Results.Select(r => r.Isin).Distinct().ToList();
var isins = fetchedItems.Select(r => r.Isin).ToList();
var existingDerivatives = await _context.TradeRepublicAssets
.OfType<DerivativeEntity>()
.Where(d => isins.Contains(d.Isin))
.ToDictionaryAsync(d => d.Isin, cancellationToken);
foreach (var item in trResponse.Results)
List<DerivativeEntity> resultEntities = new();
foreach (var item in fetchedItems)
{
if (!existingDerivatives.TryGetValue(item.Isin, out var entity))
{
@@ -438,8 +456,14 @@ public class AssetsDbService : IAssetsDbService
await _context.TradeRepublicAssets.AddAsync(entity, cancellationToken);
}
bool isShortItem = string.Equals(item.OptionType, "short", StringComparison.OrdinalIgnoreCase) ||
string.Equals(item.OptionType, "put", StringComparison.OrdinalIgnoreCase) ||
item.OptionType.Contains("short", StringComparison.OrdinalIgnoreCase) ||
item.OptionType.Contains("put", StringComparison.OrdinalIgnoreCase) ||
item.OptionType.Contains("bear", StringComparison.OrdinalIgnoreCase);
entity.UnderlyingIsin = underlyingIsin;
entity.OptionType = item.OptionType.Equals("short", StringComparison.OrdinalIgnoreCase) ? OptionType.Short : OptionType.Long;
entity.OptionType = isShortItem ? OptionType.Short : OptionType.Long;
entity.ProductCategoryName = item.ProductCategoryName;
entity.NextGenProductCategoryName = item.NextGenProductCategoryName;
entity.Strike = item.Strike ?? 0m;
@@ -449,24 +473,42 @@ public class AssetsDbService : IAssetsDbService
entity.Factor = item.Factor;
entity.Delta = item.Delta;
entity.Currency = item.Currency ?? "EUR";
entity.Expiry = DateTime.TryParse(item.Expiry, out var exp) ? exp : null;
entity.Expiry = DateTime.TryParse(item.Expiry, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AdjustToUniversal | System.Globalization.DateTimeStyles.AssumeUniversal, out var exp)
? DateTime.SpecifyKind(exp, DateTimeKind.Utc)
: (DateTime?)null;
entity.Issuer = item.Issuer;
entity.IssuerDisplayName = item.IssuerDisplayName;
entity.IssuerImageId = item.IssuerImageId;
entity.ImageId = item.ImageId;
entity.Name = $"{item.IssuerDisplayName} {item.NextGenProductCategoryName} ({item.OptionType.ToUpper()})";
entity.Name = $"{item.IssuerDisplayName} {item.NextGenProductCategoryName} ({(isShortItem ? "SHORT" : "LONG")})";
entity.LastUpdatedAt = now;
resultEntities.Add(entity);
}
await _context.SaveChangesAsync(cancellationToken);
return resultEntities;
}
return await _context.TradeRepublicAssets
// Fallback: Query from DB if Trade Republic returned 0 or was unreachable
var dbQuery = _context.TradeRepublicAssets
.OfType<DerivativeEntity>()
.AsNoTracking()
.Include(a => a.Tags)
.Where(d => d.UnderlyingIsin == underlyingIsin && d.OptionType == targetOptionType)
.Where(d => d.UnderlyingIsin == underlyingIsin && d.OptionType == targetOptionType);
if (levQuery > 0)
{
dbQuery = dbQuery.Where(d => d.Leverage >= (levQuery - 0.2m));
}
var results = await dbQuery
.OrderBy(d => d.Leverage)
.Skip(pageIndex * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return results;
}
#region Helper & Mapping Methods