refactor(assets): update asset entity mappings, database migrations, and MQTT RPC handlers

This commit is contained in:
2026-08-24 21:37:21 +02:00
parent 7060f0f7b1
commit 6974b2075b
26 changed files with 645 additions and 2237 deletions
+34 -83
View File
@@ -14,7 +14,7 @@ using Microsoft.EntityFrameworkCore;
namespace FinlyticAssets.Services;
/// <summary>
/// Defines database operations for managing Trade Republic asset entities.
/// Defines database operations for managing Trade Republic asset entities and on-demand derivatives.
/// </summary>
public interface IAssetsDbService
{
@@ -24,7 +24,6 @@ public interface IAssetsDbService
public Task<List<AssetEntity>> GetAssetsByIsinAsync(string isin);
public Task<List<AssetEntity>> GetValidAssetsByIsinAsync(string isin);
public Task<List<AssetEntity>> FindAffectedActiveAssetsAsync(string searchQuery);
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", decimal? targetLeverage = null, string? after = null, int? page = null, bool forceRefresh = false, CancellationToken cancellationToken = default);
@@ -63,7 +62,6 @@ public class AssetsDbService : IAssetsDbService
Asset = a,
Score = (a.Tags?.Count ?? 0) * 10
+ (a.HasCfd ? 5 : 0)
+ (string.IsNullOrEmpty(a.ImageId) ? 0 : 15)
+ (a.Name.Length > 3 ? 5 : 0)
})
.OrderByDescending(x => x.Score)
@@ -147,7 +145,6 @@ public class AssetsDbService : IAssetsDbService
existingEntity.Type = dtoAsset.Type;
existingEntity.InstrumentCategory = dtoAsset.InstrumentCategory;
existingEntity.HasCfd = dtoAsset.HasCfd;
existingEntity.ImageId = dtoAsset.ImageId;
existingEntity.LastUpdatedAt = now;
UpdateSubtypeProperties(existingEntity, dtoAsset);
@@ -220,14 +217,13 @@ public class AssetsDbService : IAssetsDbService
existingEntity.Type != dto.Type ||
existingEntity.InstrumentCategory != dto.InstrumentCategory ||
existingEntity.HasCfd != dto.HasCfd ||
existingEntity.ImageId != dto.ImageId ||
!existingEntity.Tags.SequenceEqual(mappedTags))
!existingEntity.Tags.Select(t => t.Id).Order().SequenceEqual(mappedTags.Select(t => t.Id).Order()) ||
HasSubtypeChanges(existingEntity, dto))
{
existingEntity.Name = dto.Name;
existingEntity.Type = dto.Type;
existingEntity.InstrumentCategory = dto.InstrumentCategory;
existingEntity.HasCfd = dto.HasCfd;
existingEntity.ImageId = dto.ImageId;
existingEntity.LastUpdatedAt = now;
existingEntity.Tags = mappedTags;
@@ -255,18 +251,8 @@ public class AssetsDbService : IAssetsDbService
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,
@@ -274,7 +260,6 @@ public class AssetsDbService : IAssetsDbService
Type = etf.Type,
InstrumentCategory = etf.InstrumentCategory,
HasCfd = etf.HasCfd,
ImageId = etf.ImageId,
DerivativeProductCategories = etf.DerivativeProductCategories?.ToList() ?? new List<string>()
},
TradeRepublicSynthetic syn => new SyntheticEntity
@@ -284,39 +269,15 @@ public class AssetsDbService : IAssetsDbService
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
HasCfd = dto.HasCfd
}
};
}
@@ -334,14 +295,21 @@ public class AssetsDbService : IAssetsDbService
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;
}
}
private static bool HasSubtypeChanges(AssetEntity entity, TradeRepublicAsset dto)
{
switch (entity)
{
case StockEntity stock when dto is TradeRepublicStock s:
return !(stock.DerivativeProductCategories?.SequenceEqual(s.DerivativeProductCategories ?? Array.Empty<string>()) ?? (s.DerivativeProductCategories == null || !s.DerivativeProductCategories.Any()));
case EtfEntity etf when dto is TradeRepublicEtf e:
return !(etf.DerivativeProductCategories?.SequenceEqual(e.DerivativeProductCategories ?? Array.Empty<string>()) ?? (e.DerivativeProductCategories == null || !e.DerivativeProductCategories.Any()));
case SyntheticEntity syn when dto is TradeRepublicSynthetic synDto:
return !(syn.DerivativeProductCategories?.SequenceEqual(synDto.DerivativeProductCategories ?? Array.Empty<string>()) ?? (synDto.DerivativeProductCategories == null || !synDto.DerivativeProductCategories.Any()));
default:
return false;
}
}
@@ -362,24 +330,6 @@ public class AssetsDbService : IAssetsDbService
.ToListAsync();
}
/// <summary>Inherits documentation from interface.</summary>
public async Task UpdateAssetImageIdAsync(string isin, string imageId)
{
var existingAssets = await _context.TradeRepublicAssets
.Where(a => a.Isin == isin)
.ToListAsync();
if (existingAssets.Count > 0)
{
foreach (var asset in existingAssets)
{
asset.ImageId = imageId;
}
await _context.SaveChangesAsync();
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetsDbService] Updated ImageId for ISIN {Isin} in database to '{ImageId}'", isin, imageId);
}
}
/// <summary>Inherits documentation from interface.</summary>
public async Task<bool> DeleteAssetAsync(string isin)
{
@@ -412,7 +362,6 @@ public class AssetsDbService : IAssetsDbService
int pageIndex = Math.Max(0, page ?? 0);
decimal levQuery = targetLeverage.HasValue && targetLeverage.Value > 0 ? targetLeverage.Value : 0m;
string trAfter = !string.IsNullOrEmpty(after) ? after : (pageIndex > 0 ? pageIndex.ToString() : "0");
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetsDbService] Fetching derivatives for {Isin} (OptionType: {Option}, Leverage: {Lev}, Page: {Page}, TR-After: {After})",
@@ -438,8 +387,7 @@ public class AssetsDbService : IAssetsDbService
{
var now = DateTime.UtcNow;
var isins = fetchedItems.Select(r => r.Isin).ToList();
var existingDerivatives = await _context.TradeRepublicAssets
.OfType<DerivativeEntity>()
var existingDerivatives = await _context.Derivatives
.Where(d => isins.Contains(d.Isin))
.ToDictionaryAsync(d => d.Isin, cancellationToken);
@@ -466,14 +414,13 @@ public class AssetsDbService : IAssetsDbService
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;
_context.TradeRepublicAssets.Update(existing);
_context.Derivatives.Update(existing);
resultEntities.Add(existing);
}
else
@@ -482,8 +429,6 @@ public class AssetsDbService : IAssetsDbService
{
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,
@@ -494,27 +439,33 @@ public class AssetsDbService : IAssetsDbService
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
LastUpdatedAt = now,
CreatedAt = now
};
await _context.TradeRepublicAssets.AddAsync(newDeriv, cancellationToken);
await _context.Derivatives.AddAsync(newDeriv, cancellationToken);
resultEntities.Add(newDeriv);
}
}
await _context.SaveChangesAsync(cancellationToken);
return resultEntities;
return resultEntities.Where(d => d.Barrier > 0 && d.Leverage > 0).ToList();
}
return await _context.TradeRepublicAssets
.OfType<DerivativeEntity>()
var dbQuery = _context.Derivatives
.AsNoTracking()
.Where(d => d.UnderlyingIsin == underlyingIsin)
.Where(d => d.UnderlyingIsin == underlyingIsin && d.OptionType == targetOptionType && d.Barrier > 0 && d.Leverage > 0);
if (targetLeverage.HasValue && targetLeverage.Value > 0)
{
dbQuery = dbQuery.OrderBy(d => Math.Abs(d.Leverage - targetLeverage.Value));
}
return await dbQuery
.Take(pageSize)
.ToListAsync(cancellationToken);
}