feat(derivatives): fix Trade Republic derivative pagination streaming and picker modal
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user