feat(assets): dynamic settings, IFinlyticLogger, live log streaming, and EF migration
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticAssets.Database;
|
||||
using FinlyticAssets.Entities;
|
||||
using FinlyticAssets.Util;
|
||||
using FinlyticCore.Dtos.TradeRepublic;
|
||||
using FinlyticCore.Services;
|
||||
using FinlyticCore.Services.TradeRepublic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -28,12 +35,12 @@ public class AssetsDbService : IAssetsDbService
|
||||
{
|
||||
private readonly AssetsDbContext _context;
|
||||
private readonly ITradeRepublicService _tradeRepublicService;
|
||||
private readonly ILogger<AssetsDbService> _logger;
|
||||
private readonly IFinlyticLogger<AssetsDbService> _finlyticLogger;
|
||||
|
||||
public AssetsDbService(AssetsDbContext context, ILogger<AssetsDbService> logger, ITradeRepublicService tradeRepublicService)
|
||||
public AssetsDbService(AssetsDbContext context, IFinlyticLogger<AssetsDbService> finlyticLogger, ITradeRepublicService tradeRepublicService)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
_finlyticLogger = finlyticLogger;
|
||||
_tradeRepublicService = tradeRepublicService;
|
||||
}
|
||||
|
||||
@@ -60,42 +67,41 @@ public class AssetsDbService : IAssetsDbService
|
||||
+ (a.Name.Length > 3 ? 5 : 0)
|
||||
})
|
||||
.OrderByDescending(x => x.Score)
|
||||
.ThenByDescending(x => x.Asset.LastUpdatedAt)
|
||||
.Take(limit)
|
||||
.Select(x => x.Asset)
|
||||
.ToList();
|
||||
|
||||
var result = new List<AssetEntity>();
|
||||
var grouped = scored.GroupBy(x => x.Asset.Type).ToList();
|
||||
|
||||
int index = 0;
|
||||
while (result.Count < limit && grouped.Any(g => g.Any()))
|
||||
{
|
||||
bool addedAny = false;
|
||||
foreach (var group in grouped)
|
||||
{
|
||||
var item = group.Skip(index).FirstOrDefault();
|
||||
if (item != null)
|
||||
{
|
||||
result.Add(item.Asset);
|
||||
addedAny = true;
|
||||
if (result.Count >= limit) break;
|
||||
}
|
||||
}
|
||||
index++;
|
||||
if (!addedAny) break;
|
||||
}
|
||||
|
||||
return result;
|
||||
return scored;
|
||||
}
|
||||
|
||||
/// <summary>Inherits documentation from interface.</summary>
|
||||
public async Task<List<AssetEntity>> GetAllValidAssetsAsync()
|
||||
{
|
||||
var cutoff = DateTime.UtcNow.AddDays(-90);
|
||||
|
||||
return await _context.TradeRepublicAssets
|
||||
.AsNoTracking()
|
||||
.Where(a => a.LastUpdatedAt >= cutoff)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>Inherits documentation from interface.</summary>
|
||||
public async Task<List<AssetEntity>> GetAssetsByIsinAsync(string isin)
|
||||
{
|
||||
return await _context.TradeRepublicAssets
|
||||
.AsNoTracking()
|
||||
.Include(a => a.Tags)
|
||||
.Where(a => a.LastUpdatedAt >= cutoff)
|
||||
.Where(a => a.Isin == isin)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>Inherits documentation from interface.</summary>
|
||||
public async Task<List<AssetEntity>> GetValidAssetsByIsinAsync(string isin)
|
||||
{
|
||||
var cutoff = DateTime.UtcNow.AddDays(-90);
|
||||
return await _context.TradeRepublicAssets
|
||||
.AsNoTracking()
|
||||
.Include(a => a.Tags)
|
||||
.Where(a => a.Isin == isin && a.LastUpdatedAt >= cutoff)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
@@ -114,7 +120,7 @@ public class AssetsDbService : IAssetsDbService
|
||||
var existingTag = await _context.TradeRepublicTags.FirstOrDefaultAsync(t => t.Id == tagDto.Id);
|
||||
if (existingTag == null)
|
||||
{
|
||||
_logger.LogTrace("Creating missing asset tag object in storage cache context. ID: {TagId}", tagDto.Id);
|
||||
await _finlyticLogger.LogTraceAsync(SettingKeys.AssetsChannel, "Creating missing asset tag object in storage cache context. ID: {TagId}", tagDto.Id);
|
||||
existingTag = new TagEntity { Id = tagDto.Id, Name = tagDto.Name, Type = tagDto.Type };
|
||||
await _context.TradeRepublicTags.AddAsync(existingTag);
|
||||
}
|
||||
@@ -124,7 +130,7 @@ public class AssetsDbService : IAssetsDbService
|
||||
|
||||
if (existingEntity == null)
|
||||
{
|
||||
_logger.LogDebug("Asset with ISIN {Isin} not found. Mapping and creating a new entity.", dtoAsset.Isin);
|
||||
await _finlyticLogger.LogDebugAsync(SettingKeys.AssetsChannel, "Asset with ISIN {Isin} not found. Mapping and creating a new entity.", dtoAsset.Isin);
|
||||
|
||||
var newEntity = MapDtoToEntity(dtoAsset);
|
||||
newEntity.LastUpdatedAt = now;
|
||||
@@ -135,7 +141,7 @@ public class AssetsDbService : IAssetsDbService
|
||||
return true;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Asset with ISIN {Isin} exists. Merging properties and updating database record.", dtoAsset.Isin);
|
||||
await _finlyticLogger.LogDebugAsync(SettingKeys.AssetsChannel, "Asset with ISIN {Isin} exists. Merging properties and updating database record.", dtoAsset.Isin);
|
||||
|
||||
existingEntity.Name = dtoAsset.Name;
|
||||
existingEntity.Type = dtoAsset.Type;
|
||||
@@ -187,7 +193,7 @@ public class AssetsDbService : IAssetsDbService
|
||||
{
|
||||
if (!tagCache.TryGetValue(tagDto.Id, out var tagEntity))
|
||||
{
|
||||
_logger.LogTrace("Creating missing asset tag object in storage cache context. ID: {TagId}", tagDto.Id);
|
||||
await _finlyticLogger.LogTraceAsync(SettingKeys.AssetsChannel, "Creating missing asset tag object in storage cache context. ID: {TagId}", tagDto.Id);
|
||||
tagEntity = new TagEntity { Id = tagDto.Id, Name = tagDto.Name, Type = tagDto.Type };
|
||||
await _context.TradeRepublicTags.AddAsync(tagEntity);
|
||||
tagCache.Add(tagDto.Id, tagEntity);
|
||||
@@ -197,7 +203,7 @@ public class AssetsDbService : IAssetsDbService
|
||||
|
||||
if (existingEntity == null)
|
||||
{
|
||||
_logger.LogDebug("Asset with ISIN {Isin} not found. Mapping and creating a new entity.", dto.Isin);
|
||||
await _finlyticLogger.LogDebugAsync(SettingKeys.AssetsChannel, "Asset with ISIN {Isin} not found. Mapping and creating a new entity.", dto.Isin);
|
||||
|
||||
var newEntity = MapDtoToEntity(dto);
|
||||
newEntity.LastUpdatedAt = now;
|
||||
@@ -208,7 +214,7 @@ public class AssetsDbService : IAssetsDbService
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Asset with ISIN {Isin} exists. Merging properties and updating database record.", dto.Isin);
|
||||
await _finlyticLogger.LogDebugAsync(SettingKeys.AssetsChannel, "Asset with ISIN {Isin} exists. Merging properties and updating database record.", dto.Isin);
|
||||
|
||||
if (existingEntity.Name != dto.Name ||
|
||||
existingEntity.Type != dto.Type ||
|
||||
@@ -223,140 +229,137 @@ public class AssetsDbService : IAssetsDbService
|
||||
existingEntity.HasCfd = dto.HasCfd;
|
||||
existingEntity.ImageId = dto.ImageId;
|
||||
existingEntity.LastUpdatedAt = now;
|
||||
|
||||
UpdateSubtypeProperties(existingEntity, dto);
|
||||
existingEntity.Tags = mappedTags;
|
||||
|
||||
UpdateSubtypeProperties(existingEntity, dto);
|
||||
_context.TradeRepublicAssets.Update(existingEntity);
|
||||
isChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isChanged)
|
||||
{
|
||||
changedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (changedCount > 0)
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
if (isChanged) changedCount++;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return changedCount;
|
||||
}
|
||||
|
||||
/// <summary>Inherits documentation from interface.</summary>
|
||||
public async Task<List<AssetEntity>> GetAssetsByIsinAsync(string isin)
|
||||
private static AssetEntity MapDtoToEntity(TradeRepublicAsset dto)
|
||||
{
|
||||
var localAssets = await _context.TradeRepublicAssets
|
||||
.Include(a => a.Tags)
|
||||
.Where(a => a.Isin == isin)
|
||||
.ToListAsync();
|
||||
|
||||
if (localAssets.Count > 0)
|
||||
return dto switch
|
||||
{
|
||||
return localAssets;
|
||||
}
|
||||
|
||||
// JIT-Fetch via API
|
||||
var trAssetDto = await _tradeRepublicService.GetAsset(isin);
|
||||
if (trAssetDto?.Results != null && trAssetDto.Results.Count > 0)
|
||||
{
|
||||
foreach (var asset in trAssetDto.Results)
|
||||
TradeRepublicStock stock => new StockEntity
|
||||
{
|
||||
await AddOrUpdateAssetAsync(asset);
|
||||
Isin = stock.Isin,
|
||||
Name = stock.Name,
|
||||
Type = stock.Type,
|
||||
InstrumentCategory = stock.InstrumentCategory,
|
||||
HasCfd = stock.HasCfd,
|
||||
ImageId = stock.ImageId,
|
||||
DerivativeProductCategories = stock.DerivativeProductCategories?.ToList() ?? new List<string>()
|
||||
},
|
||||
TradeRepublicCrypto crypto => new CryptoEntity
|
||||
{
|
||||
Isin = crypto.Isin,
|
||||
Name = crypto.Name,
|
||||
Type = crypto.Type,
|
||||
InstrumentCategory = crypto.InstrumentCategory,
|
||||
HasCfd = crypto.HasCfd,
|
||||
ImageId = crypto.ImageId
|
||||
},
|
||||
TradeRepublicEtf etf => new EtfEntity
|
||||
{
|
||||
Isin = etf.Isin,
|
||||
Name = etf.Name,
|
||||
Type = etf.Type,
|
||||
InstrumentCategory = etf.InstrumentCategory,
|
||||
HasCfd = etf.HasCfd,
|
||||
ImageId = etf.ImageId,
|
||||
DerivativeProductCategories = etf.DerivativeProductCategories?.ToList() ?? new List<string>()
|
||||
},
|
||||
TradeRepublicSynthetic syn => new SyntheticEntity
|
||||
{
|
||||
Isin = syn.Isin,
|
||||
Name = syn.Name,
|
||||
Type = syn.Type,
|
||||
InstrumentCategory = syn.InstrumentCategory,
|
||||
HasCfd = syn.HasCfd,
|
||||
ImageId = syn.ImageId,
|
||||
DerivativeProductCategories = syn.DerivativeProductCategories?.ToList() ?? new List<string>()
|
||||
},
|
||||
TradeRepublicBond bond => new BondEntity
|
||||
{
|
||||
Isin = bond.Isin,
|
||||
Name = bond.Name,
|
||||
Type = bond.Type,
|
||||
InstrumentCategory = bond.InstrumentCategory,
|
||||
HasCfd = bond.HasCfd,
|
||||
ImageId = bond.ImageId,
|
||||
BondIssuerName = bond.BondIssuerName,
|
||||
SearchSubtitle = bond.SearchSubtitle
|
||||
},
|
||||
TradeRepublicDerivative deriv => new DerivativeEntity
|
||||
{
|
||||
Isin = deriv.Isin,
|
||||
Name = deriv.Name,
|
||||
Type = deriv.Type,
|
||||
InstrumentCategory = deriv.InstrumentCategory,
|
||||
HasCfd = deriv.HasCfd,
|
||||
ImageId = deriv.ImageId,
|
||||
UnderlyingIsin = deriv.UnderlyingIsin,
|
||||
DerivativeProductCategories = deriv.DerivativeProductCategories?.ToList() ?? new List<string>()
|
||||
},
|
||||
_ => new StockEntity
|
||||
{
|
||||
Isin = dto.Isin,
|
||||
Name = dto.Name,
|
||||
Type = dto.Type,
|
||||
InstrumentCategory = dto.InstrumentCategory,
|
||||
HasCfd = dto.HasCfd,
|
||||
ImageId = dto.ImageId
|
||||
}
|
||||
|
||||
return await _context.TradeRepublicAssets
|
||||
.Include(a => a.Tags)
|
||||
.Where(a => a.Isin == isin)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Inherits documentation from interface.</summary>
|
||||
public async Task<List<AssetEntity>> GetValidAssetsByIsinAsync(string isin)
|
||||
private static void UpdateSubtypeProperties(AssetEntity entity, TradeRepublicAsset dto)
|
||||
{
|
||||
var cutoff = DateTime.UtcNow.AddDays(-14);
|
||||
|
||||
return await _context.TradeRepublicAssets
|
||||
.AsNoTracking()
|
||||
.Include(a => a.Tags)
|
||||
.Where(a => a.Isin == isin && a.LastUpdatedAt >= cutoff)
|
||||
.ToListAsync();
|
||||
switch (entity)
|
||||
{
|
||||
case StockEntity stock when dto is TradeRepublicStock s:
|
||||
stock.DerivativeProductCategories = s.DerivativeProductCategories?.ToList() ?? new List<string>();
|
||||
break;
|
||||
case EtfEntity etf when dto is TradeRepublicEtf e:
|
||||
etf.DerivativeProductCategories = e.DerivativeProductCategories?.ToList() ?? new List<string>();
|
||||
break;
|
||||
case SyntheticEntity syn when dto is TradeRepublicSynthetic synDto:
|
||||
syn.DerivativeProductCategories = synDto.DerivativeProductCategories?.ToList() ?? new List<string>();
|
||||
break;
|
||||
case BondEntity bond when dto is TradeRepublicBond b:
|
||||
bond.BondIssuerName = b.BondIssuerName;
|
||||
bond.SearchSubtitle = b.SearchSubtitle;
|
||||
break;
|
||||
case DerivativeEntity deriv when dto is TradeRepublicDerivative d:
|
||||
deriv.UnderlyingIsin = d.UnderlyingIsin;
|
||||
deriv.DerivativeProductCategories = d.DerivativeProductCategories?.ToList() ?? new List<string>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Inherits documentation from interface.</summary>
|
||||
public async Task<List<AssetEntity>> FindAffectedActiveAssetsAsync(string searchQuery)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(searchQuery)) return [];
|
||||
var cutoff = DateTime.UtcNow.AddDays(-90);
|
||||
string cleanQuery = searchQuery.Trim().ToLowerInvariant();
|
||||
|
||||
var cutoff = DateTime.UtcNow.AddDays(-14);
|
||||
|
||||
var searchTerms = searchQuery
|
||||
.Split(',')
|
||||
.Select(t => t.Trim())
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (searchTerms.Count == 0) return [];
|
||||
|
||||
var query = _context.TradeRepublicAssets
|
||||
return await _context.TradeRepublicAssets
|
||||
.AsNoTracking()
|
||||
.Where(a => a.LastUpdatedAt >= cutoff)
|
||||
.Include(a => a.Tags)
|
||||
.AsQueryable();
|
||||
|
||||
foreach (var term in searchTerms)
|
||||
{
|
||||
var lowerTerm = term.ToLower();
|
||||
query = query.Where(a =>
|
||||
a.Isin.ToLower().Contains(lowerTerm) ||
|
||||
a.Name.ToLower().Contains(lowerTerm) ||
|
||||
a.Tags.Any(tag => tag.Name.ToLower().Contains(lowerTerm)));
|
||||
}
|
||||
|
||||
var localAssets = await query.ToListAsync();
|
||||
|
||||
var possibleIsins = searchTerms
|
||||
.Where(t => t.Length == 12 && char.IsLetter(t[0]) && char.IsLetter(t[1]))
|
||||
.Select(t => t.ToUpper())
|
||||
.ToList();
|
||||
|
||||
if (possibleIsins.Count > 0)
|
||||
{
|
||||
var foundIsins = localAssets.Select(a => a.Isin).ToHashSet();
|
||||
var missingIsins = possibleIsins.Where(isin => !foundIsins.Contains(isin)).ToList();
|
||||
|
||||
if (missingIsins.Count > 0)
|
||||
{
|
||||
var fetchedNewAsset = false;
|
||||
foreach (var missingIsin in missingIsins)
|
||||
{
|
||||
var trAssetDto = await _tradeRepublicService.GetAsset(missingIsin);
|
||||
if (trAssetDto?.Results != null)
|
||||
{
|
||||
foreach (var asset in trAssetDto.Results)
|
||||
{
|
||||
await AddOrUpdateAssetAsync(asset);
|
||||
}
|
||||
|
||||
fetchedNewAsset = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (fetchedNewAsset)
|
||||
{
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return localAssets;
|
||||
.Where(a => a.LastUpdatedAt >= cutoff && (
|
||||
a.Isin.ToLower().Contains(cleanQuery) ||
|
||||
a.Name.ToLower().Contains(cleanQuery)
|
||||
))
|
||||
.Take(25)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>Inherits documentation from interface.</summary>
|
||||
@@ -373,7 +376,7 @@ public class AssetsDbService : IAssetsDbService
|
||||
asset.ImageId = imageId;
|
||||
}
|
||||
await _context.SaveChangesAsync();
|
||||
_logger.LogInformation("[{Channel}] Updated ImageId for ISIN {Isin} in database to '{ImageId}'", "AssetsChannel", isin, imageId);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetsDbService] Updated ImageId for ISIN {Isin} in database to '{ImageId}'", isin, imageId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,13 +386,13 @@ public class AssetsDbService : IAssetsDbService
|
||||
var asset = await _context.TradeRepublicAssets.FirstOrDefaultAsync(a => a.Isin == isin);
|
||||
if (asset == null)
|
||||
{
|
||||
_logger.LogWarning("[{Channel}] Delete execution cancelled. Asset with ISIN {Isin} does not exist.", "AssetsChannel", isin);
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.AssetsChannel, "[AssetsDbService] Delete execution cancelled. Asset with ISIN {Isin} does not exist.", isin);
|
||||
return false;
|
||||
}
|
||||
|
||||
_context.TradeRepublicAssets.Remove(asset);
|
||||
await _context.SaveChangesAsync();
|
||||
_logger.LogInformation("[{Channel}] Asset with ISIN {Isin} has been successfully deleted.", "AssetsChannel", isin);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetsDbService] Asset with ISIN {Isin} has been successfully deleted.", isin);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -410,11 +413,10 @@ public class AssetsDbService : IAssetsDbService
|
||||
|
||||
decimal levQuery = targetLeverage.HasValue && targetLeverage.Value > 0 ? targetLeverage.Value : 0m;
|
||||
|
||||
// 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);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetsDbService] Fetching derivatives for {Isin} (OptionType: {Option}, Leverage: {Lev}, Page: {Page}, TR-After: {After})",
|
||||
underlyingIsin, cleanOptionType, levQuery, pageIndex, trAfter);
|
||||
|
||||
var trReq = new TradeRepublicDerivativesRequest(
|
||||
Underlying: underlyingIsin,
|
||||
@@ -429,8 +431,8 @@ public class AssetsDbService : IAssetsDbService
|
||||
var trResponse = await _tradeRepublicService.GetDerivativesAsync(trReq, cancellationToken);
|
||||
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");
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetsDbService] TR returned {Count} derivatives for {Isin} (Cursors.After: {NextAfter})",
|
||||
fetchedItems.Count, underlyingIsin, trResponse?.Cursors?.After ?? "null");
|
||||
|
||||
if (fetchedItems.Count > 0)
|
||||
{
|
||||
@@ -445,144 +447,75 @@ public class AssetsDbService : IAssetsDbService
|
||||
|
||||
foreach (var item in fetchedItems)
|
||||
{
|
||||
if (!existingDerivatives.TryGetValue(item.Isin, out var entity))
|
||||
DateTime? expiryDate = null;
|
||||
if (!string.IsNullOrWhiteSpace(item.Expiry) && DateTime.TryParse(item.Expiry, out var parsedExp))
|
||||
{
|
||||
entity = new DerivativeEntity
|
||||
{
|
||||
Isin = item.Isin,
|
||||
InstrumentCategory = "derivative",
|
||||
Type = "derivative"
|
||||
};
|
||||
await _context.TradeRepublicAssets.AddAsync(entity, cancellationToken);
|
||||
expiryDate = parsedExp.ToUniversalTime();
|
||||
}
|
||||
|
||||
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);
|
||||
if (existingDerivatives.TryGetValue(item.Isin, out var existing))
|
||||
{
|
||||
existing.Name = !string.IsNullOrWhiteSpace(item.ProductCategoryName) ? item.ProductCategoryName : item.Isin;
|
||||
existing.UnderlyingIsin = underlyingIsin;
|
||||
existing.Strike = item.Strike ?? 0m;
|
||||
existing.Barrier = item.Barrier ?? 0m;
|
||||
existing.Leverage = item.Leverage ?? 0m;
|
||||
existing.Expiry = expiryDate;
|
||||
existing.OptionType = targetOptionType;
|
||||
existing.ProductCategoryName = item.ProductCategoryName;
|
||||
existing.NextGenProductCategoryName = item.NextGenProductCategoryName;
|
||||
existing.Issuer = item.Issuer;
|
||||
existing.IssuerDisplayName = item.IssuerDisplayName;
|
||||
existing.IssuerImageId = item.IssuerImageId;
|
||||
existing.Size = item.Size;
|
||||
existing.Factor = item.Factor;
|
||||
existing.Delta = item.Delta;
|
||||
existing.Currency = item.Currency;
|
||||
existing.LastUpdatedAt = now;
|
||||
|
||||
entity.UnderlyingIsin = underlyingIsin;
|
||||
entity.OptionType = isShortItem ? OptionType.Short : OptionType.Long;
|
||||
entity.ProductCategoryName = item.ProductCategoryName;
|
||||
entity.NextGenProductCategoryName = item.NextGenProductCategoryName;
|
||||
entity.Strike = item.Strike ?? 0m;
|
||||
entity.Barrier = item.Barrier ?? 0m;
|
||||
entity.Leverage = item.Leverage ?? 0m;
|
||||
entity.Size = item.Size;
|
||||
entity.Factor = item.Factor;
|
||||
entity.Delta = item.Delta;
|
||||
entity.Currency = item.Currency ?? "EUR";
|
||||
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} ({(isShortItem ? "SHORT" : "LONG")})";
|
||||
entity.LastUpdatedAt = now;
|
||||
_context.TradeRepublicAssets.Update(existing);
|
||||
resultEntities.Add(existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
var newDeriv = new DerivativeEntity
|
||||
{
|
||||
Isin = item.Isin,
|
||||
Name = !string.IsNullOrWhiteSpace(item.ProductCategoryName) ? item.ProductCategoryName : item.Isin,
|
||||
Type = "derivative",
|
||||
InstrumentCategory = "derivative",
|
||||
UnderlyingIsin = underlyingIsin,
|
||||
Strike = item.Strike ?? 0m,
|
||||
Barrier = item.Barrier ?? 0m,
|
||||
Leverage = item.Leverage ?? 0m,
|
||||
Expiry = expiryDate,
|
||||
OptionType = targetOptionType,
|
||||
ProductCategoryName = item.ProductCategoryName,
|
||||
NextGenProductCategoryName = item.NextGenProductCategoryName,
|
||||
Issuer = item.Issuer,
|
||||
IssuerDisplayName = item.IssuerDisplayName,
|
||||
IssuerImageId = item.IssuerImageId,
|
||||
Size = item.Size,
|
||||
Factor = item.Factor,
|
||||
Delta = item.Delta,
|
||||
Currency = item.Currency,
|
||||
LastUpdatedAt = now
|
||||
};
|
||||
|
||||
resultEntities.Add(entity);
|
||||
await _context.TradeRepublicAssets.AddAsync(newDeriv, cancellationToken);
|
||||
resultEntities.Add(newDeriv);
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return resultEntities;
|
||||
}
|
||||
|
||||
// Fallback: Query from DB if Trade Republic returned 0 or was unreachable
|
||||
var dbQuery = _context.TradeRepublicAssets
|
||||
return await _context.TradeRepublicAssets
|
||||
.OfType<DerivativeEntity>()
|
||||
.AsNoTracking()
|
||||
.Include(a => a.Tags)
|
||||
.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)
|
||||
.Where(d => d.UnderlyingIsin == underlyingIsin)
|
||||
.Take(pageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
#region Helper & Mapping Methods
|
||||
|
||||
private AssetEntity MapDtoToEntity(TradeRepublicAsset dto)
|
||||
{
|
||||
AssetEntity entity = dto switch
|
||||
{
|
||||
TradeRepublicStock stock => new StockEntity
|
||||
{ Isin = stock.Isin, DerivativeProductCategories = stock.DerivativeProductCategories.ToList() },
|
||||
TradeRepublicCrypto crypto => new CryptoEntity
|
||||
{ Isin = crypto.Isin, Subtitle = crypto.Subtitle, SearchSubtitle = crypto.SearchSubtitle },
|
||||
TradeRepublicEtf etf => new EtfEntity
|
||||
{
|
||||
Isin = etf.Isin, EtfDescription = etf.EtfDescription, MappedEtfIndexName = etf.MappedEtfIndexName,
|
||||
Subtitle = etf.Subtitle, SearchSubtitle = etf.SearchSubtitle,
|
||||
DerivativeProductCategories = etf.DerivativeProductCategories.ToList()
|
||||
},
|
||||
TradeRepublicSynthetic synth => new SyntheticEntity
|
||||
{ Isin = synth.Isin, DerivativeProductCategories = synth.DerivativeProductCategories.ToList() },
|
||||
TradeRepublicBond bond => new BondEntity
|
||||
{ Isin = bond.Isin, BondIssuerName = bond.BondIssuerName, SearchSubtitle = bond.SearchSubtitle },
|
||||
TradeRepublicDerivative deriv => new DerivativeEntity
|
||||
{
|
||||
Isin = deriv.Isin, UnderlyingIsin = deriv.UnderlyingIsin,
|
||||
DerivativeProductCategories = deriv.DerivativeProductCategories.ToList()
|
||||
},
|
||||
_ => throw new NotSupportedException($"Type {dto.GetType().Name} is not supported.")
|
||||
};
|
||||
|
||||
return PopulateBaseProperties(entity, dto);
|
||||
}
|
||||
|
||||
private AssetEntity PopulateBaseProperties(AssetEntity entity, TradeRepublicAsset dto)
|
||||
{
|
||||
entity.Name = dto.Name;
|
||||
entity.Type = dto.Type;
|
||||
entity.InstrumentCategory = dto.InstrumentCategory;
|
||||
entity.HasCfd = dto.HasCfd;
|
||||
entity.ImageId = dto.ImageId;
|
||||
return entity;
|
||||
}
|
||||
|
||||
private void UpdateSubtypeProperties(AssetEntity entity, TradeRepublicAsset dto)
|
||||
{
|
||||
switch (entity)
|
||||
{
|
||||
case StockEntity stockEntity when dto is TradeRepublicStock stockDto:
|
||||
stockEntity.DerivativeProductCategories = stockDto.DerivativeProductCategories.ToList();
|
||||
break;
|
||||
case CryptoEntity cryptoEntity when dto is TradeRepublicCrypto cryptoDto:
|
||||
cryptoEntity.Subtitle = cryptoDto.Subtitle;
|
||||
cryptoEntity.SearchSubtitle = cryptoDto.SearchSubtitle;
|
||||
break;
|
||||
case EtfEntity etfEntity when dto is TradeRepublicEtf etfDto:
|
||||
etfEntity.DerivativeProductCategories = etfDto.DerivativeProductCategories.ToList();
|
||||
etfEntity.EtfDescription = etfDto.EtfDescription;
|
||||
etfEntity.MappedEtfIndexName = etfDto.MappedEtfIndexName;
|
||||
etfEntity.Subtitle = etfDto.Subtitle;
|
||||
etfEntity.SearchSubtitle = etfDto.SearchSubtitle;
|
||||
break;
|
||||
case SyntheticEntity synthEntity when dto is TradeRepublicSynthetic synthDto:
|
||||
synthEntity.DerivativeProductCategories = synthDto.DerivativeProductCategories.ToList();
|
||||
break;
|
||||
case BondEntity bondEntity when dto is TradeRepublicBond bondDto:
|
||||
bondEntity.BondIssuerName = bondDto.BondIssuerName;
|
||||
bondEntity.SearchSubtitle = bondDto.SearchSubtitle;
|
||||
break;
|
||||
case DerivativeEntity derivEntity when dto is TradeRepublicDerivative derivDto:
|
||||
derivEntity.DerivativeProductCategories = derivDto.DerivativeProductCategories.ToList();
|
||||
derivEntity.UnderlyingIsin = derivDto.UnderlyingIsin;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user