refactor(assets): update asset entity mappings, database migrations, and MQTT RPC handlers
This commit is contained in:
@@ -17,9 +17,9 @@ public class AssetsDbContext : DbContext, ISettingsDbContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
public DbSet<SettingEntity> DynamicSettings => Set<SettingEntity>();
|
public DbSet<SettingEntity> DynamicSettings => Set<SettingEntity>();
|
||||||
public DbSet<Settings> Settings { get; set; }
|
public DbSet<AssetEntity> TradeRepublicAssets => Set<AssetEntity>();
|
||||||
public DbSet<AssetEntity> TradeRepublicAssets { get; set; }
|
public DbSet<DerivativeEntity> Derivatives => Set<DerivativeEntity>();
|
||||||
public DbSet<TagEntity> TradeRepublicTags { get; set; }
|
public DbSet<TagEntity> TradeRepublicTags => Set<TagEntity>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -30,20 +30,29 @@ public class AssetsDbContext : DbContext, ISettingsDbContext
|
|||||||
entity.HasKey(e => e.Id);
|
entity.HasKey(e => e.Id);
|
||||||
entity.HasIndex(e => e.Key).IsUnique();
|
entity.HasIndex(e => e.Key).IsUnique();
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<AssetEntity>(entity =>
|
modelBuilder.Entity<AssetEntity>(entity =>
|
||||||
{
|
{
|
||||||
entity.HasKey(e => new {e.Isin, e.InstrumentCategory});
|
entity.HasKey(e => new { e.Isin, e.InstrumentCategory });
|
||||||
entity.HasIndex(e => e.LastUpdatedAt);
|
entity.HasIndex(e => e.LastUpdatedAt);
|
||||||
|
|
||||||
entity.HasDiscriminator<string>("AssetType")
|
entity.HasDiscriminator<string>("AssetType")
|
||||||
.HasValue<StockEntity>("Stock")
|
.HasValue<StockEntity>("Stock")
|
||||||
.HasValue<CryptoEntity>("Crypto")
|
|
||||||
.HasValue<EtfEntity>("Etf")
|
.HasValue<EtfEntity>("Etf")
|
||||||
.HasValue<SyntheticEntity>("Synthetic")
|
.HasValue<SyntheticEntity>("Synthetic");
|
||||||
.HasValue<BondEntity>("Bond")
|
|
||||||
.HasValue<DerivativeEntity>("Derivative");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<DerivativeEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Isin);
|
||||||
|
entity.HasIndex(e => e.UnderlyingIsin);
|
||||||
|
entity.HasIndex(e => e.OptionType);
|
||||||
|
entity.HasIndex(e => e.Leverage);
|
||||||
|
entity.HasIndex(e => e.LastUpdatedAt);
|
||||||
|
entity.HasIndex(e => new { e.UnderlyingIsin, e.OptionType, e.Leverage, e.Barrier });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
var stringListConverter =
|
var stringListConverter =
|
||||||
new Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<List<string>, string>(
|
new Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<List<string>, string>(
|
||||||
v => System.Text.Json.JsonSerializer.Serialize(v, (System.Text.Json.JsonSerializerOptions?)null),
|
v => System.Text.Json.JsonSerializer.Serialize(v, (System.Text.Json.JsonSerializerOptions?)null),
|
||||||
@@ -75,15 +84,6 @@ public class AssetsDbContext : DbContext, ISettingsDbContext
|
|||||||
|
|
||||||
modelBuilder.Entity<TagEntity>(entity => { entity.HasKey(e => e.Id); });
|
modelBuilder.Entity<TagEntity>(entity => { entity.HasKey(e => e.Id); });
|
||||||
|
|
||||||
modelBuilder.Entity<Settings>(entity =>
|
|
||||||
{
|
|
||||||
entity.HasKey(e => e.Id);
|
|
||||||
|
|
||||||
entity.Property(e => e.CurrentScanningType)
|
|
||||||
.HasConversion<string>()
|
|
||||||
.HasMaxLength(50);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity<AssetEntity>()
|
modelBuilder.Entity<AssetEntity>()
|
||||||
.HasMany(a => a.Tags)
|
.HasMany(a => a.Tags)
|
||||||
.WithMany(t => t.Assets);
|
.WithMany(t => t.Assets);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ WORKDIR /app
|
|||||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
COPY ["FinlyticCore/FinlyticCore.csproj", "FinlyticCore/"]
|
||||||
COPY ["FinlyticAssets/FinlyticAssets.csproj", "FinlyticAssets/"]
|
COPY ["FinlyticAssets/FinlyticAssets.csproj", "FinlyticAssets/"]
|
||||||
RUN dotnet restore "FinlyticAssets/FinlyticAssets.csproj"
|
RUN dotnet restore "FinlyticAssets/FinlyticAssets.csproj"
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ public abstract class AssetEntity
|
|||||||
public string Type { get; set; } = string.Empty;
|
public string Type { get; set; } = string.Empty;
|
||||||
public string InstrumentCategory { get; set; } = string.Empty;
|
public string InstrumentCategory { get; set; } = string.Empty;
|
||||||
public bool HasCfd { get; set; }
|
public bool HasCfd { get; set; }
|
||||||
public string? ImageId { get; set; }
|
|
||||||
|
|
||||||
public DateTime LastUpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime LastUpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
public List<TagEntity> Tags { get; set; } = new();
|
public List<TagEntity> Tags { get; set; } = new();
|
||||||
|
|||||||
@@ -11,11 +11,17 @@ public enum OptionType
|
|||||||
Short
|
Short
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DerivativeEntity : AssetEntity
|
public class DerivativeEntity
|
||||||
{
|
{
|
||||||
|
[Key]
|
||||||
|
[StringLength(12, MinimumLength = 12)]
|
||||||
|
public string Isin { get; set; } = string.Empty;
|
||||||
|
|
||||||
// --- Verknüpfung zum Basiswert (Underlying) ---
|
// --- Verknüpfung zum Basiswert (Underlying) ---
|
||||||
[StringLength(12, MinimumLength = 12)]
|
[StringLength(12, MinimumLength = 12)]
|
||||||
public string? UnderlyingIsin { get; set; }
|
public string UnderlyingIsin { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
// --- Derivat-Spezifikationen ---
|
// --- Derivat-Spezifikationen ---
|
||||||
public OptionType OptionType { get; set; } // Long / Short
|
public OptionType OptionType { get; set; } // Long / Short
|
||||||
@@ -54,7 +60,8 @@ public class DerivativeEntity : AssetEntity
|
|||||||
[MaxLength(150)]
|
[MaxLength(150)]
|
||||||
public string IssuerDisplayName { get; set; } = string.Empty;
|
public string IssuerDisplayName { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string? IssuerImageId { get; set; }
|
public DateTime LastUpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
// PostgreSQL Mapped Array für TR-Kategorien
|
// PostgreSQL Mapped Array für TR-Kategorien
|
||||||
public List<string> DerivativeProductCategories { get; set; } = new();
|
public List<string> DerivativeProductCategories { get; set; } = new();
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using FinlyticAssets.Models;
|
|
||||||
using FinlyticCore.Models.Assets;
|
|
||||||
|
|
||||||
namespace FinlyticAssets.Entities;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the global synchronization and timing configurations for the Trade Republic asset scanner.
|
|
||||||
/// </summary>
|
|
||||||
public class Settings
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the unique identifier for the settings record.
|
|
||||||
/// </summary>
|
|
||||||
[Key]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether the very first full scan of all assets has been completed.
|
|
||||||
/// Used to switch from fast initial discovery delays to stealthy incremental update delays.
|
|
||||||
/// </summary>
|
|
||||||
public bool FinishedInitialScan { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the maximum number of assets requested per single API pagination call.
|
|
||||||
/// Values around 100 look like standard dynamic-scrolling payloads from a real client device.
|
|
||||||
/// </summary>
|
|
||||||
public int TradeRepublicMaxRequestPageSize { get; set; } = 100;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the idle delay in seconds between switching from one full asset category to another (e.g., from Stocks to Crypto)
|
|
||||||
/// during standard maintenance mode. (Default 12000s = ~3.3 hours).
|
|
||||||
/// </summary>
|
|
||||||
public int AssetUpdateTypeDelay { get; set; } = 12000;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the idle delay in seconds between switching asset categories during the initial setup scan.
|
|
||||||
/// Set to 0 to move immediately to the next type after finishing the current one. (Default 0s).
|
|
||||||
/// </summary>
|
|
||||||
public int InitAssetUpdateTypeDelay { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the baseline delay in seconds between sequential page requests of the same asset type during the initial setup scan.
|
|
||||||
/// (Default 120s = 2 minutes).
|
|
||||||
/// </summary>
|
|
||||||
public int InitBatchAssetUpdateDelay { get; set; } = 120;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the standard baseline delay in seconds between sequential page requests of the same asset type during recurring incremental updates.
|
|
||||||
/// Spreads pagination widely over time to blend into regular human traffic profiles. (Default 600s = 10 minutes).
|
|
||||||
/// </summary>
|
|
||||||
public int BatchAssetUpdateDelay { get; set; } = 600;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the asset type that is currently being processed by the full scan.
|
|
||||||
/// Acts as a live pointer for recovery after a service interruption.
|
|
||||||
/// </summary>
|
|
||||||
public AssetType CurrentScanningType { get; set; } = AssetType.Stock;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the page number of the <see cref="CurrentScanningType"/> that is currently being fetched or was just processed.
|
|
||||||
/// Trade Republic uses 1-based pagination. A value of 0 means the scan is currently idle or between types.
|
|
||||||
/// </summary>
|
|
||||||
public int CurrentScanningPage { get; set; } = 0;
|
|
||||||
}
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using FinlyticAssets.Database;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace FinlyticAssets.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(AssetsDbContext))]
|
|
||||||
[Migration("20260801073314_Init")]
|
|
||||||
partial class Init
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.9")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("AssetEntityTagEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("TagsId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("AssetsIsin")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("AssetsInstrumentCategory")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("TagsId", "AssetsIsin", "AssetsInstrumentCategory");
|
|
||||||
|
|
||||||
b.HasIndex("AssetsIsin", "AssetsInstrumentCategory");
|
|
||||||
|
|
||||||
b.ToTable("AssetEntityTagEntity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.Settings", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<int>("AssetUpdateTypeDelay")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("BatchAssetUpdateDelay")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("CurrentScanningPage")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("CurrentScanningType")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("character varying(50)");
|
|
||||||
|
|
||||||
b.Property<bool>("FinishedInitialScan")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<int>("InitAssetUpdateTypeDelay")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("InitBatchAssetUpdateDelay")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("TradeRepublicMaxRequestPageSize")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Settings");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticCore.Entities.Assets.AssetEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Isin")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("InstrumentCategory")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("AssetType")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(13)
|
|
||||||
.HasColumnType("character varying(13)");
|
|
||||||
|
|
||||||
b.Property<bool>("HasCfd")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("ImageId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTime>("LastUpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Type")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Isin", "InstrumentCategory");
|
|
||||||
|
|
||||||
b.HasIndex("LastUpdatedAt");
|
|
||||||
|
|
||||||
b.ToTable("TradeRepublicAssets");
|
|
||||||
|
|
||||||
b.HasDiscriminator<string>("AssetType").HasValue("AssetEntity");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticCore.Entities.Assets.TagEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Type")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("TradeRepublicTags");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticCore.Entities.Assets.BondEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticCore.Entities.Assets.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<string>("BondIssuerName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("SearchSubtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Bond");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticCore.Entities.Assets.CryptoEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticCore.Entities.Assets.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<string>("SearchSubtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Subtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.ToTable("TradeRepublicAssets", t =>
|
|
||||||
{
|
|
||||||
t.Property("SearchSubtitle")
|
|
||||||
.HasColumnName("CryptoEntity_SearchSubtitle");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Crypto");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticCore.Entities.Assets.DerivativeEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticCore.Entities.Assets.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<string>("DerivativeProductCategories")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("UnderlyingIsin")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Derivative");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticCore.Entities.Assets.EtfEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticCore.Entities.Assets.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<string>("DerivativeProductCategories")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("EtfDescription")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("MappedEtfIndexName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("SearchSubtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Subtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.ToTable("TradeRepublicAssets", t =>
|
|
||||||
{
|
|
||||||
t.Property("DerivativeProductCategories")
|
|
||||||
.HasColumnName("EtfEntity_DerivativeProductCategories");
|
|
||||||
|
|
||||||
t.Property("SearchSubtitle")
|
|
||||||
.HasColumnName("EtfEntity_SearchSubtitle");
|
|
||||||
|
|
||||||
t.Property("Subtitle")
|
|
||||||
.HasColumnName("EtfEntity_Subtitle");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Etf");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticCore.Entities.Assets.StockEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticCore.Entities.Assets.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<string>("DerivativeProductCategories")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.ToTable("TradeRepublicAssets", t =>
|
|
||||||
{
|
|
||||||
t.Property("DerivativeProductCategories")
|
|
||||||
.HasColumnName("StockEntity_DerivativeProductCategories");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Stock");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticCore.Entities.Assets.SyntheticEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticCore.Entities.Assets.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<string>("DerivativeProductCategories")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.ToTable("TradeRepublicAssets", t =>
|
|
||||||
{
|
|
||||||
t.Property("DerivativeProductCategories")
|
|
||||||
.HasColumnName("SyntheticEntity_DerivativeProductCategories");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Synthetic");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("AssetEntityTagEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("FinlyticCore.Entities.Assets.TagEntity", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("TagsId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("FinlyticCore.Entities.Assets.AssetEntity", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("AssetsIsin", "AssetsInstrumentCategory")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace FinlyticAssets.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class UpdateDerivativeAssets : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AlterColumn<string>(
|
|
||||||
name: "UnderlyingIsin",
|
|
||||||
table: "TradeRepublicAssets",
|
|
||||||
type: "character varying(12)",
|
|
||||||
maxLength: 12,
|
|
||||||
nullable: true,
|
|
||||||
oldClrType: typeof(string),
|
|
||||||
oldType: "text",
|
|
||||||
oldNullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<decimal>(
|
|
||||||
name: "Barrier",
|
|
||||||
table: "TradeRepublicAssets",
|
|
||||||
type: "numeric(18,6)",
|
|
||||||
nullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "Currency",
|
|
||||||
table: "TradeRepublicAssets",
|
|
||||||
type: "character varying(10)",
|
|
||||||
maxLength: 10,
|
|
||||||
nullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<decimal>(
|
|
||||||
name: "Delta",
|
|
||||||
table: "TradeRepublicAssets",
|
|
||||||
type: "numeric",
|
|
||||||
nullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<DateTime>(
|
|
||||||
name: "Expiry",
|
|
||||||
table: "TradeRepublicAssets",
|
|
||||||
type: "timestamp with time zone",
|
|
||||||
nullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<decimal>(
|
|
||||||
name: "Factor",
|
|
||||||
table: "TradeRepublicAssets",
|
|
||||||
type: "numeric",
|
|
||||||
nullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "Issuer",
|
|
||||||
table: "TradeRepublicAssets",
|
|
||||||
type: "character varying(150)",
|
|
||||||
maxLength: 150,
|
|
||||||
nullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "IssuerDisplayName",
|
|
||||||
table: "TradeRepublicAssets",
|
|
||||||
type: "character varying(150)",
|
|
||||||
maxLength: 150,
|
|
||||||
nullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "IssuerImageId",
|
|
||||||
table: "TradeRepublicAssets",
|
|
||||||
type: "text",
|
|
||||||
nullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<decimal>(
|
|
||||||
name: "Leverage",
|
|
||||||
table: "TradeRepublicAssets",
|
|
||||||
type: "numeric(10,4)",
|
|
||||||
nullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "NextGenProductCategoryName",
|
|
||||||
table: "TradeRepublicAssets",
|
|
||||||
type: "character varying(100)",
|
|
||||||
maxLength: 100,
|
|
||||||
nullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<int>(
|
|
||||||
name: "OptionType",
|
|
||||||
table: "TradeRepublicAssets",
|
|
||||||
type: "integer",
|
|
||||||
nullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "ProductCategoryName",
|
|
||||||
table: "TradeRepublicAssets",
|
|
||||||
type: "character varying(100)",
|
|
||||||
maxLength: 100,
|
|
||||||
nullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<decimal>(
|
|
||||||
name: "Size",
|
|
||||||
table: "TradeRepublicAssets",
|
|
||||||
type: "numeric",
|
|
||||||
nullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<decimal>(
|
|
||||||
name: "Strike",
|
|
||||||
table: "TradeRepublicAssets",
|
|
||||||
type: "numeric(18,6)",
|
|
||||||
nullable: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Barrier",
|
|
||||||
table: "TradeRepublicAssets");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Currency",
|
|
||||||
table: "TradeRepublicAssets");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Delta",
|
|
||||||
table: "TradeRepublicAssets");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Expiry",
|
|
||||||
table: "TradeRepublicAssets");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Factor",
|
|
||||||
table: "TradeRepublicAssets");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Issuer",
|
|
||||||
table: "TradeRepublicAssets");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "IssuerDisplayName",
|
|
||||||
table: "TradeRepublicAssets");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "IssuerImageId",
|
|
||||||
table: "TradeRepublicAssets");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Leverage",
|
|
||||||
table: "TradeRepublicAssets");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "NextGenProductCategoryName",
|
|
||||||
table: "TradeRepublicAssets");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "OptionType",
|
|
||||||
table: "TradeRepublicAssets");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "ProductCategoryName",
|
|
||||||
table: "TradeRepublicAssets");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Size",
|
|
||||||
table: "TradeRepublicAssets");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Strike",
|
|
||||||
table: "TradeRepublicAssets");
|
|
||||||
|
|
||||||
migrationBuilder.AlterColumn<string>(
|
|
||||||
name: "UnderlyingIsin",
|
|
||||||
table: "TradeRepublicAssets",
|
|
||||||
type: "text",
|
|
||||||
nullable: true,
|
|
||||||
oldClrType: typeof(string),
|
|
||||||
oldType: "character varying(12)",
|
|
||||||
oldMaxLength: 12,
|
|
||||||
oldNullable: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using FinlyticAssets.Database;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace FinlyticAssets.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(AssetsDbContext))]
|
|
||||||
[Migration("20260813205059_AddDynamicSettings")]
|
|
||||||
partial class AddDynamicSettings
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.9")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("AssetEntityTagEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("TagsId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("AssetsIsin")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("AssetsInstrumentCategory")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("TagsId", "AssetsIsin", "AssetsInstrumentCategory");
|
|
||||||
|
|
||||||
b.HasIndex("AssetsIsin", "AssetsInstrumentCategory");
|
|
||||||
|
|
||||||
b.ToTable("AssetEntityTagEntity");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.AssetEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Isin")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("InstrumentCategory")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("AssetType")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(13)
|
|
||||||
.HasColumnType("character varying(13)");
|
|
||||||
|
|
||||||
b.Property<bool>("HasCfd")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("ImageId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTime>("LastUpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Type")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Isin", "InstrumentCategory");
|
|
||||||
|
|
||||||
b.HasIndex("LastUpdatedAt");
|
|
||||||
|
|
||||||
b.ToTable("TradeRepublicAssets");
|
|
||||||
|
|
||||||
b.HasDiscriminator<string>("AssetType").HasValue("AssetEntity");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.Settings", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<int>("AssetUpdateTypeDelay")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("BatchAssetUpdateDelay")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("CurrentScanningPage")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("CurrentScanningType")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("character varying(50)");
|
|
||||||
|
|
||||||
b.Property<bool>("FinishedInitialScan")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<int>("InitAssetUpdateTypeDelay")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("InitBatchAssetUpdateDelay")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("TradeRepublicMaxRequestPageSize")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Settings");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.TagEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Type")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("TradeRepublicTags");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticCore.Entities.Settings.SettingEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("Key")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(150)
|
|
||||||
.HasColumnType("character varying(150)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("LastUpdatedUtc")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ServiceIdentifier")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("character varying(100)");
|
|
||||||
|
|
||||||
b.Property<string>("ValueJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Key");
|
|
||||||
|
|
||||||
b.ToTable("DynamicSettings");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.BondEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<string>("BondIssuerName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("SearchSubtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Bond");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.CryptoEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<string>("SearchSubtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Subtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.ToTable("TradeRepublicAssets", t =>
|
|
||||||
{
|
|
||||||
t.Property("SearchSubtitle")
|
|
||||||
.HasColumnName("CryptoEntity_SearchSubtitle");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Crypto");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.DerivativeEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<decimal>("Barrier")
|
|
||||||
.HasColumnType("numeric(18,6)");
|
|
||||||
|
|
||||||
b.Property<string>("Currency")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(10)
|
|
||||||
.HasColumnType("character varying(10)");
|
|
||||||
|
|
||||||
b.Property<decimal?>("Delta")
|
|
||||||
.HasColumnType("numeric");
|
|
||||||
|
|
||||||
b.Property<string>("DerivativeProductCategories")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("Expiry")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<decimal?>("Factor")
|
|
||||||
.HasColumnType("numeric");
|
|
||||||
|
|
||||||
b.Property<string>("Issuer")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(150)
|
|
||||||
.HasColumnType("character varying(150)");
|
|
||||||
|
|
||||||
b.Property<string>("IssuerDisplayName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(150)
|
|
||||||
.HasColumnType("character varying(150)");
|
|
||||||
|
|
||||||
b.Property<string>("IssuerImageId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<decimal>("Leverage")
|
|
||||||
.HasColumnType("numeric(10,4)");
|
|
||||||
|
|
||||||
b.Property<string>("NextGenProductCategoryName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("character varying(100)");
|
|
||||||
|
|
||||||
b.Property<int>("OptionType")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("ProductCategoryName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("character varying(100)");
|
|
||||||
|
|
||||||
b.Property<decimal?>("Size")
|
|
||||||
.HasColumnType("numeric");
|
|
||||||
|
|
||||||
b.Property<decimal>("Strike")
|
|
||||||
.HasColumnType("numeric(18,6)");
|
|
||||||
|
|
||||||
b.Property<string>("UnderlyingIsin")
|
|
||||||
.HasMaxLength(12)
|
|
||||||
.HasColumnType("character varying(12)");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Derivative");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.EtfEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<string>("DerivativeProductCategories")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("EtfDescription")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("MappedEtfIndexName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("SearchSubtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Subtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.ToTable("TradeRepublicAssets", t =>
|
|
||||||
{
|
|
||||||
t.Property("DerivativeProductCategories")
|
|
||||||
.HasColumnName("EtfEntity_DerivativeProductCategories");
|
|
||||||
|
|
||||||
t.Property("SearchSubtitle")
|
|
||||||
.HasColumnName("EtfEntity_SearchSubtitle");
|
|
||||||
|
|
||||||
t.Property("Subtitle")
|
|
||||||
.HasColumnName("EtfEntity_Subtitle");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Etf");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.StockEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<string>("DerivativeProductCategories")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.ToTable("TradeRepublicAssets", t =>
|
|
||||||
{
|
|
||||||
t.Property("DerivativeProductCategories")
|
|
||||||
.HasColumnName("StockEntity_DerivativeProductCategories");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Stock");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.SyntheticEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<string>("DerivativeProductCategories")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.ToTable("TradeRepublicAssets", t =>
|
|
||||||
{
|
|
||||||
t.Property("DerivativeProductCategories")
|
|
||||||
.HasColumnName("SyntheticEntity_DerivativeProductCategories");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Synthetic");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("AssetEntityTagEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("FinlyticAssets.Entities.TagEntity", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("TagsId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("FinlyticAssets.Entities.AssetEntity", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("AssetsIsin", "AssetsInstrumentCategory")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace FinlyticAssets.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddDynamicSettings : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "DynamicSettings",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
Key = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: false),
|
|
||||||
ValueJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ServiceIdentifier = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
|
||||||
LastUpdatedUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_DynamicSettings", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_DynamicSettings_Key",
|
|
||||||
table: "DynamicSettings",
|
|
||||||
column: "Key");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "DynamicSettings");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace FinlyticAssets.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class UpdateDynamicSettingsUniqueIndex : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.Sql(@"
|
|
||||||
DELETE FROM ""DynamicSettings"" a USING ""DynamicSettings"" b
|
|
||||||
WHERE a.""Key"" = b.""Key"" AND a.""Id"" < b.""Id"";
|
|
||||||
");
|
|
||||||
|
|
||||||
migrationBuilder.DropIndex(
|
|
||||||
name: "IX_DynamicSettings_Key",
|
|
||||||
table: "DynamicSettings");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_DynamicSettings_Key",
|
|
||||||
table: "DynamicSettings",
|
|
||||||
column: "Key",
|
|
||||||
unique: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropIndex(
|
|
||||||
name: "IX_DynamicSettings_Key",
|
|
||||||
table: "DynamicSettings");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_DynamicSettings_Key",
|
|
||||||
table: "DynamicSettings",
|
|
||||||
column: "Key");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+75
-143
@@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||||||
namespace FinlyticAssets.Migrations
|
namespace FinlyticAssets.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(AssetsDbContext))]
|
[DbContext(typeof(AssetsDbContext))]
|
||||||
[Migration("20260815184053_UpdateDynamicSettingsUniqueIndex")]
|
[Migration("20260818185753_Init")]
|
||||||
partial class UpdateDynamicSettingsUniqueIndex
|
partial class Init
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
@@ -59,9 +59,6 @@ namespace FinlyticAssets.Migrations
|
|||||||
b.Property<bool>("HasCfd")
|
b.Property<bool>("HasCfd")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("ImageId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTime>("LastUpdatedAt")
|
b.Property<DateTime>("LastUpdatedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
@@ -84,41 +81,91 @@ namespace FinlyticAssets.Migrations
|
|||||||
b.UseTphMappingStrategy();
|
b.UseTphMappingStrategy();
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.Settings", b =>
|
modelBuilder.Entity("FinlyticAssets.Entities.DerivativeEntity", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<string>("Isin")
|
||||||
.ValueGeneratedOnAdd()
|
.HasMaxLength(12)
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("character varying(12)");
|
||||||
|
|
||||||
b.Property<int>("AssetUpdateTypeDelay")
|
b.Property<decimal>("Barrier")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("numeric(18,6)");
|
||||||
|
|
||||||
b.Property<int>("BatchAssetUpdateDelay")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<int>("CurrentScanningPage")
|
b.Property<string>("Currency")
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("CurrentScanningType")
|
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(50)
|
.HasMaxLength(10)
|
||||||
.HasColumnType("character varying(50)");
|
.HasColumnType("character varying(10)");
|
||||||
|
|
||||||
b.Property<bool>("FinishedInitialScan")
|
b.Property<decimal?>("Delta")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
b.Property<int>("InitAssetUpdateTypeDelay")
|
b.Property<string>("DerivativeProductCategories")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Expiry")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Factor")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("Issuer")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(150)
|
||||||
|
.HasColumnType("character varying(150)");
|
||||||
|
|
||||||
|
b.Property<string>("IssuerDisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(150)
|
||||||
|
.HasColumnType("character varying(150)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal>("Leverage")
|
||||||
|
.HasColumnType("numeric(10,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("NextGenProductCategoryName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<int>("OptionType")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<int>("InitBatchAssetUpdateDelay")
|
b.Property<string>("ProductCategoryName")
|
||||||
.HasColumnType("integer");
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
b.Property<int>("TradeRepublicMaxRequestPageSize")
|
b.Property<decimal?>("Size")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.Property<decimal>("Strike")
|
||||||
|
.HasColumnType("numeric(18,6)");
|
||||||
|
|
||||||
b.ToTable("Settings");
|
b.Property<string>("UnderlyingIsin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(12)
|
||||||
|
.HasColumnType("character varying(12)");
|
||||||
|
|
||||||
|
b.HasKey("Isin");
|
||||||
|
|
||||||
|
b.HasIndex("LastUpdatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("Leverage");
|
||||||
|
|
||||||
|
b.HasIndex("OptionType");
|
||||||
|
|
||||||
|
b.HasIndex("UnderlyingIsin");
|
||||||
|
|
||||||
|
b.ToTable("Derivatives");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.TagEntity", b =>
|
modelBuilder.Entity("FinlyticAssets.Entities.TagEntity", b =>
|
||||||
@@ -170,109 +217,6 @@ namespace FinlyticAssets.Migrations
|
|||||||
b.ToTable("DynamicSettings");
|
b.ToTable("DynamicSettings");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.BondEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<string>("BondIssuerName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("SearchSubtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Bond");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.CryptoEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<string>("SearchSubtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Subtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.ToTable("TradeRepublicAssets", t =>
|
|
||||||
{
|
|
||||||
t.Property("SearchSubtitle")
|
|
||||||
.HasColumnName("CryptoEntity_SearchSubtitle");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Crypto");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.DerivativeEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<decimal>("Barrier")
|
|
||||||
.HasColumnType("numeric(18,6)");
|
|
||||||
|
|
||||||
b.Property<string>("Currency")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(10)
|
|
||||||
.HasColumnType("character varying(10)");
|
|
||||||
|
|
||||||
b.Property<decimal?>("Delta")
|
|
||||||
.HasColumnType("numeric");
|
|
||||||
|
|
||||||
b.Property<string>("DerivativeProductCategories")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("Expiry")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<decimal?>("Factor")
|
|
||||||
.HasColumnType("numeric");
|
|
||||||
|
|
||||||
b.Property<string>("Issuer")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(150)
|
|
||||||
.HasColumnType("character varying(150)");
|
|
||||||
|
|
||||||
b.Property<string>("IssuerDisplayName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(150)
|
|
||||||
.HasColumnType("character varying(150)");
|
|
||||||
|
|
||||||
b.Property<string>("IssuerImageId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<decimal>("Leverage")
|
|
||||||
.HasColumnType("numeric(10,4)");
|
|
||||||
|
|
||||||
b.Property<string>("NextGenProductCategoryName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("character varying(100)");
|
|
||||||
|
|
||||||
b.Property<int>("OptionType")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("ProductCategoryName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("character varying(100)");
|
|
||||||
|
|
||||||
b.Property<decimal?>("Size")
|
|
||||||
.HasColumnType("numeric");
|
|
||||||
|
|
||||||
b.Property<decimal>("Strike")
|
|
||||||
.HasColumnType("numeric(18,6)");
|
|
||||||
|
|
||||||
b.Property<string>("UnderlyingIsin")
|
|
||||||
.HasMaxLength(12)
|
|
||||||
.HasColumnType("character varying(12)");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Derivative");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.EtfEntity", b =>
|
modelBuilder.Entity("FinlyticAssets.Entities.EtfEntity", b =>
|
||||||
{
|
{
|
||||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
||||||
@@ -297,18 +241,6 @@ namespace FinlyticAssets.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.ToTable("TradeRepublicAssets", t =>
|
|
||||||
{
|
|
||||||
t.Property("DerivativeProductCategories")
|
|
||||||
.HasColumnName("EtfEntity_DerivativeProductCategories");
|
|
||||||
|
|
||||||
t.Property("SearchSubtitle")
|
|
||||||
.HasColumnName("EtfEntity_SearchSubtitle");
|
|
||||||
|
|
||||||
t.Property("Subtitle")
|
|
||||||
.HasColumnName("EtfEntity_Subtitle");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Etf");
|
b.HasDiscriminator().HasValue("Etf");
|
||||||
});
|
});
|
||||||
|
|
||||||
+69
-22
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
@@ -12,22 +12,47 @@ namespace FinlyticAssets.Migrations
|
|||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
name: "Settings",
|
name: "Derivatives",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
Isin = table.Column<string>(type: "character varying(12)", maxLength: 12, nullable: false),
|
||||||
FinishedInitialScan = table.Column<bool>(type: "boolean", nullable: false),
|
UnderlyingIsin = table.Column<string>(type: "character varying(12)", maxLength: 12, nullable: false),
|
||||||
TradeRepublicMaxRequestPageSize = table.Column<int>(type: "integer", nullable: false),
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
AssetUpdateTypeDelay = table.Column<int>(type: "integer", nullable: false),
|
OptionType = table.Column<int>(type: "integer", nullable: false),
|
||||||
InitAssetUpdateTypeDelay = table.Column<int>(type: "integer", nullable: false),
|
ProductCategoryName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
InitBatchAssetUpdateDelay = table.Column<int>(type: "integer", nullable: false),
|
NextGenProductCategoryName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
BatchAssetUpdateDelay = table.Column<int>(type: "integer", nullable: false),
|
Strike = table.Column<decimal>(type: "numeric(18,6)", nullable: false),
|
||||||
CurrentScanningType = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
Barrier = table.Column<decimal>(type: "numeric(18,6)", nullable: false),
|
||||||
CurrentScanningPage = table.Column<int>(type: "integer", nullable: false)
|
Leverage = table.Column<decimal>(type: "numeric(10,4)", nullable: false),
|
||||||
|
Size = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
Factor = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
Delta = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
Currency = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
|
||||||
|
Expiry = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
Issuer = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: false),
|
||||||
|
IssuerDisplayName = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: false),
|
||||||
|
LastUpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
DerivativeProductCategories = table.Column<string>(type: "text", nullable: false)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
table.PrimaryKey("PK_Settings", x => x.Id);
|
table.PrimaryKey("PK_Derivatives", x => x.Isin);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "DynamicSettings",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Key = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: false),
|
||||||
|
ValueJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
ServiceIdentifier = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
LastUpdatedUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_DynamicSettings", x => x.Id);
|
||||||
});
|
});
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@@ -39,20 +64,13 @@ namespace FinlyticAssets.Migrations
|
|||||||
Name = table.Column<string>(type: "text", nullable: false),
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
Type = table.Column<string>(type: "text", nullable: false),
|
Type = table.Column<string>(type: "text", nullable: false),
|
||||||
HasCfd = table.Column<bool>(type: "boolean", nullable: false),
|
HasCfd = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
ImageId = table.Column<string>(type: "text", nullable: true),
|
|
||||||
LastUpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
LastUpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
AssetType = table.Column<string>(type: "character varying(13)", maxLength: 13, nullable: false),
|
AssetType = table.Column<string>(type: "character varying(13)", maxLength: 13, nullable: false),
|
||||||
BondIssuerName = table.Column<string>(type: "text", nullable: true),
|
|
||||||
SearchSubtitle = table.Column<string>(type: "text", nullable: true),
|
|
||||||
Subtitle = table.Column<string>(type: "text", nullable: true),
|
|
||||||
CryptoEntity_SearchSubtitle = table.Column<string>(type: "text", nullable: true),
|
|
||||||
DerivativeProductCategories = table.Column<string>(type: "text", nullable: true),
|
DerivativeProductCategories = table.Column<string>(type: "text", nullable: true),
|
||||||
UnderlyingIsin = table.Column<string>(type: "text", nullable: true),
|
|
||||||
EtfEntity_DerivativeProductCategories = table.Column<string>(type: "text", nullable: true),
|
|
||||||
EtfDescription = table.Column<string>(type: "text", nullable: true),
|
EtfDescription = table.Column<string>(type: "text", nullable: true),
|
||||||
MappedEtfIndexName = table.Column<string>(type: "text", nullable: true),
|
MappedEtfIndexName = table.Column<string>(type: "text", nullable: true),
|
||||||
EtfEntity_Subtitle = table.Column<string>(type: "text", nullable: true),
|
Subtitle = table.Column<string>(type: "text", nullable: true),
|
||||||
EtfEntity_SearchSubtitle = table.Column<string>(type: "text", nullable: true),
|
SearchSubtitle = table.Column<string>(type: "text", nullable: true),
|
||||||
StockEntity_DerivativeProductCategories = table.Column<string>(type: "text", nullable: true),
|
StockEntity_DerivativeProductCategories = table.Column<string>(type: "text", nullable: true),
|
||||||
SyntheticEntity_DerivativeProductCategories = table.Column<string>(type: "text", nullable: true)
|
SyntheticEntity_DerivativeProductCategories = table.Column<string>(type: "text", nullable: true)
|
||||||
},
|
},
|
||||||
@@ -104,6 +122,32 @@ namespace FinlyticAssets.Migrations
|
|||||||
table: "AssetEntityTagEntity",
|
table: "AssetEntityTagEntity",
|
||||||
columns: new[] { "AssetsIsin", "AssetsInstrumentCategory" });
|
columns: new[] { "AssetsIsin", "AssetsInstrumentCategory" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Derivatives_LastUpdatedAt",
|
||||||
|
table: "Derivatives",
|
||||||
|
column: "LastUpdatedAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Derivatives_Leverage",
|
||||||
|
table: "Derivatives",
|
||||||
|
column: "Leverage");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Derivatives_OptionType",
|
||||||
|
table: "Derivatives",
|
||||||
|
column: "OptionType");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Derivatives_UnderlyingIsin",
|
||||||
|
table: "Derivatives",
|
||||||
|
column: "UnderlyingIsin");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_DynamicSettings_Key",
|
||||||
|
table: "DynamicSettings",
|
||||||
|
column: "Key",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_TradeRepublicAssets_LastUpdatedAt",
|
name: "IX_TradeRepublicAssets_LastUpdatedAt",
|
||||||
table: "TradeRepublicAssets",
|
table: "TradeRepublicAssets",
|
||||||
@@ -117,7 +161,10 @@ namespace FinlyticAssets.Migrations
|
|||||||
name: "AssetEntityTagEntity");
|
name: "AssetEntityTagEntity");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "Settings");
|
name: "Derivatives");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "DynamicSettings");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "TradeRepublicAssets");
|
name: "TradeRepublicAssets");
|
||||||
+77
-112
@@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||||||
namespace FinlyticAssets.Migrations
|
namespace FinlyticAssets.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(AssetsDbContext))]
|
[DbContext(typeof(AssetsDbContext))]
|
||||||
[Migration("20260813202348_UpdateDerivativeAssets")]
|
[Migration("20260821153552_SyncAssetModelDrift")]
|
||||||
partial class UpdateDerivativeAssets
|
partial class SyncAssetModelDrift
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
@@ -59,9 +59,6 @@ namespace FinlyticAssets.Migrations
|
|||||||
b.Property<bool>("HasCfd")
|
b.Property<bool>("HasCfd")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("ImageId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTime>("LastUpdatedAt")
|
b.Property<DateTime>("LastUpdatedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
@@ -84,104 +81,18 @@ namespace FinlyticAssets.Migrations
|
|||||||
b.UseTphMappingStrategy();
|
b.UseTphMappingStrategy();
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.Settings", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<int>("AssetUpdateTypeDelay")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("BatchAssetUpdateDelay")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("CurrentScanningPage")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("CurrentScanningType")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("character varying(50)");
|
|
||||||
|
|
||||||
b.Property<bool>("FinishedInitialScan")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<int>("InitAssetUpdateTypeDelay")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("InitBatchAssetUpdateDelay")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("TradeRepublicMaxRequestPageSize")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Settings");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.TagEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Type")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("TradeRepublicTags");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.BondEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<string>("BondIssuerName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("SearchSubtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Bond");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.CryptoEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<string>("SearchSubtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Subtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.ToTable("TradeRepublicAssets", t =>
|
|
||||||
{
|
|
||||||
t.Property("SearchSubtitle")
|
|
||||||
.HasColumnName("CryptoEntity_SearchSubtitle");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Crypto");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.DerivativeEntity", b =>
|
modelBuilder.Entity("FinlyticAssets.Entities.DerivativeEntity", b =>
|
||||||
{
|
{
|
||||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
b.Property<string>("Isin")
|
||||||
|
.HasMaxLength(12)
|
||||||
|
.HasColumnType("character varying(12)");
|
||||||
|
|
||||||
b.Property<decimal>("Barrier")
|
b.Property<decimal>("Barrier")
|
||||||
.HasColumnType("numeric(18,6)");
|
.HasColumnType("numeric(18,6)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<string>("Currency")
|
b.Property<string>("Currency")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(10)
|
.HasMaxLength(10)
|
||||||
@@ -210,12 +121,16 @@ namespace FinlyticAssets.Migrations
|
|||||||
.HasMaxLength(150)
|
.HasMaxLength(150)
|
||||||
.HasColumnType("character varying(150)");
|
.HasColumnType("character varying(150)");
|
||||||
|
|
||||||
b.Property<string>("IssuerImageId")
|
b.Property<DateTime>("LastUpdatedAt")
|
||||||
.HasColumnType("text");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<decimal>("Leverage")
|
b.Property<decimal>("Leverage")
|
||||||
.HasColumnType("numeric(10,4)");
|
.HasColumnType("numeric(10,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("NextGenProductCategoryName")
|
b.Property<string>("NextGenProductCategoryName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(100)
|
.HasMaxLength(100)
|
||||||
@@ -236,10 +151,72 @@ namespace FinlyticAssets.Migrations
|
|||||||
.HasColumnType("numeric(18,6)");
|
.HasColumnType("numeric(18,6)");
|
||||||
|
|
||||||
b.Property<string>("UnderlyingIsin")
|
b.Property<string>("UnderlyingIsin")
|
||||||
|
.IsRequired()
|
||||||
.HasMaxLength(12)
|
.HasMaxLength(12)
|
||||||
.HasColumnType("character varying(12)");
|
.HasColumnType("character varying(12)");
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Derivative");
|
b.HasKey("Isin");
|
||||||
|
|
||||||
|
b.HasIndex("LastUpdatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("Leverage");
|
||||||
|
|
||||||
|
b.HasIndex("OptionType");
|
||||||
|
|
||||||
|
b.HasIndex("UnderlyingIsin");
|
||||||
|
|
||||||
|
b.HasIndex("UnderlyingIsin", "OptionType", "Leverage", "Barrier");
|
||||||
|
|
||||||
|
b.ToTable("Derivatives");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticAssets.Entities.TagEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("TradeRepublicTags");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticCore.Entities.Settings.SettingEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(150)
|
||||||
|
.HasColumnType("character varying(150)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdatedUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("ServiceIdentifier")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("ValueJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Key")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("DynamicSettings");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.EtfEntity", b =>
|
modelBuilder.Entity("FinlyticAssets.Entities.EtfEntity", b =>
|
||||||
@@ -266,18 +243,6 @@ namespace FinlyticAssets.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.ToTable("TradeRepublicAssets", t =>
|
|
||||||
{
|
|
||||||
t.Property("DerivativeProductCategories")
|
|
||||||
.HasColumnName("EtfEntity_DerivativeProductCategories");
|
|
||||||
|
|
||||||
t.Property("SearchSubtitle")
|
|
||||||
.HasColumnName("EtfEntity_SearchSubtitle");
|
|
||||||
|
|
||||||
t.Property("Subtitle")
|
|
||||||
.HasColumnName("EtfEntity_Subtitle");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Etf");
|
b.HasDiscriminator().HasValue("Etf");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FinlyticAssets.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class SyncAssetModelDrift : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Derivatives_UnderlyingIsin_OptionType_Leverage_Barrier",
|
||||||
|
table: "Derivatives",
|
||||||
|
columns: new[] { "UnderlyingIsin", "OptionType", "Leverage", "Barrier" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Derivatives_UnderlyingIsin_OptionType_Leverage_Barrier",
|
||||||
|
table: "Derivatives");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,9 +56,6 @@ namespace FinlyticAssets.Migrations
|
|||||||
b.Property<bool>("HasCfd")
|
b.Property<bool>("HasCfd")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("ImageId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTime>("LastUpdatedAt")
|
b.Property<DateTime>("LastUpdatedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
@@ -81,41 +78,93 @@ namespace FinlyticAssets.Migrations
|
|||||||
b.UseTphMappingStrategy();
|
b.UseTphMappingStrategy();
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.Settings", b =>
|
modelBuilder.Entity("FinlyticAssets.Entities.DerivativeEntity", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<string>("Isin")
|
||||||
.ValueGeneratedOnAdd()
|
.HasMaxLength(12)
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("character varying(12)");
|
||||||
|
|
||||||
b.Property<int>("AssetUpdateTypeDelay")
|
b.Property<decimal>("Barrier")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("numeric(18,6)");
|
||||||
|
|
||||||
b.Property<int>("BatchAssetUpdateDelay")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<int>("CurrentScanningPage")
|
b.Property<string>("Currency")
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("CurrentScanningType")
|
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(50)
|
.HasMaxLength(10)
|
||||||
.HasColumnType("character varying(50)");
|
.HasColumnType("character varying(10)");
|
||||||
|
|
||||||
b.Property<bool>("FinishedInitialScan")
|
b.Property<decimal?>("Delta")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
b.Property<int>("InitAssetUpdateTypeDelay")
|
b.Property<string>("DerivativeProductCategories")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Expiry")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Factor")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("Issuer")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(150)
|
||||||
|
.HasColumnType("character varying(150)");
|
||||||
|
|
||||||
|
b.Property<string>("IssuerDisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(150)
|
||||||
|
.HasColumnType("character varying(150)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal>("Leverage")
|
||||||
|
.HasColumnType("numeric(10,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("NextGenProductCategoryName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<int>("OptionType")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<int>("InitBatchAssetUpdateDelay")
|
b.Property<string>("ProductCategoryName")
|
||||||
.HasColumnType("integer");
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
b.Property<int>("TradeRepublicMaxRequestPageSize")
|
b.Property<decimal?>("Size")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.Property<decimal>("Strike")
|
||||||
|
.HasColumnType("numeric(18,6)");
|
||||||
|
|
||||||
b.ToTable("Settings");
|
b.Property<string>("UnderlyingIsin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(12)
|
||||||
|
.HasColumnType("character varying(12)");
|
||||||
|
|
||||||
|
b.HasKey("Isin");
|
||||||
|
|
||||||
|
b.HasIndex("LastUpdatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("Leverage");
|
||||||
|
|
||||||
|
b.HasIndex("OptionType");
|
||||||
|
|
||||||
|
b.HasIndex("UnderlyingIsin");
|
||||||
|
|
||||||
|
b.HasIndex("UnderlyingIsin", "OptionType", "Leverage", "Barrier");
|
||||||
|
|
||||||
|
b.ToTable("Derivatives");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.TagEntity", b =>
|
modelBuilder.Entity("FinlyticAssets.Entities.TagEntity", b =>
|
||||||
@@ -167,109 +216,6 @@ namespace FinlyticAssets.Migrations
|
|||||||
b.ToTable("DynamicSettings");
|
b.ToTable("DynamicSettings");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.BondEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<string>("BondIssuerName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("SearchSubtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Bond");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.CryptoEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<string>("SearchSubtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Subtitle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.ToTable("TradeRepublicAssets", t =>
|
|
||||||
{
|
|
||||||
t.Property("SearchSubtitle")
|
|
||||||
.HasColumnName("CryptoEntity_SearchSubtitle");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Crypto");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.DerivativeEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
|
||||||
|
|
||||||
b.Property<decimal>("Barrier")
|
|
||||||
.HasColumnType("numeric(18,6)");
|
|
||||||
|
|
||||||
b.Property<string>("Currency")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(10)
|
|
||||||
.HasColumnType("character varying(10)");
|
|
||||||
|
|
||||||
b.Property<decimal?>("Delta")
|
|
||||||
.HasColumnType("numeric");
|
|
||||||
|
|
||||||
b.Property<string>("DerivativeProductCategories")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("Expiry")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<decimal?>("Factor")
|
|
||||||
.HasColumnType("numeric");
|
|
||||||
|
|
||||||
b.Property<string>("Issuer")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(150)
|
|
||||||
.HasColumnType("character varying(150)");
|
|
||||||
|
|
||||||
b.Property<string>("IssuerDisplayName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(150)
|
|
||||||
.HasColumnType("character varying(150)");
|
|
||||||
|
|
||||||
b.Property<string>("IssuerImageId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<decimal>("Leverage")
|
|
||||||
.HasColumnType("numeric(10,4)");
|
|
||||||
|
|
||||||
b.Property<string>("NextGenProductCategoryName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("character varying(100)");
|
|
||||||
|
|
||||||
b.Property<int>("OptionType")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("ProductCategoryName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("character varying(100)");
|
|
||||||
|
|
||||||
b.Property<decimal?>("Size")
|
|
||||||
.HasColumnType("numeric");
|
|
||||||
|
|
||||||
b.Property<decimal>("Strike")
|
|
||||||
.HasColumnType("numeric(18,6)");
|
|
||||||
|
|
||||||
b.Property<string>("UnderlyingIsin")
|
|
||||||
.HasMaxLength(12)
|
|
||||||
.HasColumnType("character varying(12)");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Derivative");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("FinlyticAssets.Entities.EtfEntity", b =>
|
modelBuilder.Entity("FinlyticAssets.Entities.EtfEntity", b =>
|
||||||
{
|
{
|
||||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
||||||
@@ -294,18 +240,6 @@ namespace FinlyticAssets.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.ToTable("TradeRepublicAssets", t =>
|
|
||||||
{
|
|
||||||
t.Property("DerivativeProductCategories")
|
|
||||||
.HasColumnName("EtfEntity_DerivativeProductCategories");
|
|
||||||
|
|
||||||
t.Property("SearchSubtitle")
|
|
||||||
.HasColumnName("EtfEntity_SearchSubtitle");
|
|
||||||
|
|
||||||
t.Property("Subtitle")
|
|
||||||
.HasColumnName("EtfEntity_Subtitle");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Etf");
|
b.HasDiscriminator().HasValue("Etf");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using FinlyticAssets.Database;
|
using FinlyticAssets.Database;
|
||||||
using FinlyticAssets.Services;
|
using FinlyticAssets.Services;
|
||||||
using FinlyticAssets.Util;
|
using FinlyticAssets.Util;
|
||||||
@@ -24,14 +23,12 @@ builder.Services.AddSingleton(typeof(IFinlyticLogger<>), typeof(FinlyticLogger<>
|
|||||||
builder.Services.AddScoped<TradeRepublicClient>();
|
builder.Services.AddScoped<TradeRepublicClient>();
|
||||||
builder.Services.AddScoped<ITradeRepublicService, TradeRepublicService>();
|
builder.Services.AddScoped<ITradeRepublicService, TradeRepublicService>();
|
||||||
builder.Services.AddScoped<IAssetsDbService, AssetsDbService>();
|
builder.Services.AddScoped<IAssetsDbService, AssetsDbService>();
|
||||||
builder.Services.AddScoped<ISettingsDbService, SettingsDbService>();
|
|
||||||
builder.Services.AddScoped<IAssetsIndexService, AssetsIndexService>();
|
builder.Services.AddScoped<IAssetsIndexService, AssetsIndexService>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<AssetsMqttClient>();
|
builder.Services.AddSingleton<AssetsMqttClient>();
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<AssetsMqttClient>());
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<AssetsMqttClient>());
|
||||||
|
|
||||||
builder.Services.AddHostedService<AssetScannerBackgroundService>();
|
builder.Services.AddHostedService<AssetScannerBackgroundService>();
|
||||||
builder.Services.AddHostedService<LogoFetcherBackgroundService>();
|
|
||||||
|
|
||||||
var host = builder.Build();
|
var host = builder.Build();
|
||||||
|
|
||||||
@@ -40,21 +37,14 @@ using (var scope = host.Services.CreateScope())
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var context = scope.ServiceProvider.GetRequiredService<AssetsDbContext>();
|
var context = scope.ServiceProvider.GetRequiredService<AssetsDbContext>();
|
||||||
await context.Database.MigrateAsync();
|
var connStr = builder.Configuration.GetConnectionString("DefaultConnection") ?? "";
|
||||||
|
await context.MigrateWithBootstrapAsync(connStr);
|
||||||
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
|
|
||||||
var settings = await settingsService.GetSettings();
|
|
||||||
if (settings.InitAssetUpdateTypeDelay == 3600)
|
|
||||||
{
|
|
||||||
settings.InitAssetUpdateTypeDelay = 0;
|
|
||||||
await settingsService.SaveSettings(settings);
|
|
||||||
Console.WriteLine("[Startup] InitAssetUpdateTypeDelay reset from 3600 to 0.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Critical error during database migration for FinlyticAssets: {ex.Message}");
|
Console.WriteLine($"Critical error during database migration for FinlyticAssets: {ex.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
host.Run();
|
host.Run();
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
# Finlytic Assets Service
|
|
||||||
|
|
||||||
Finlytic Assets is a scalable C# microservice designed for asset discovery, metadata ingestion, and quick querying. It interfaces directly with the Trade Republic API via WebSockets and exposes a high-performance RPC interface over MQTT to other services in the Finlytic ecosystem.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
TR[Trade Republic WebSocket API] <-->|WS Protocol| TRS[TradeRepublicService]
|
|
||||||
TRS <-->|Ingest| ADS[AssetsFullScanService]
|
|
||||||
ADS <-->|Save / Update| DB[PostgreSQL Database]
|
|
||||||
DB -->|Trigger Index Update| AIS[AssetsIndexService]
|
|
||||||
AIS -->|Write Cache| Index[assets/index/index.json]
|
|
||||||
|
|
||||||
MS[Other Finlytic Services] <-->|MQTT Request-Reply| MqttClient[AssetsMqttClient]
|
|
||||||
MqttClient <-->|Query Cache| DB
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Core Features & Workflows
|
|
||||||
|
|
||||||
### 1. Automated WebSocket scraping
|
|
||||||
- **Continuous Scan**: The background worker (`AssetsFullScanService`) iterates through all supported asset types:
|
|
||||||
- **Stocks** (`StockEntity`)
|
|
||||||
- **Crypto** (`CryptoEntity`)
|
|
||||||
- **Derivatives** (`DerivativeEntity`)
|
|
||||||
- **Bonds** (`BondEntity`)
|
|
||||||
- **Funds/ETFs** (`EtfEntity`)
|
|
||||||
- **Resilient Reconnection**: Uses a robust custom socket wrapper (`TradeRepublicClient`) that closes automatically after 5 minutes of inactivity to mimic human interaction profiles and reconnects dynamically when a request is made.
|
|
||||||
|
|
||||||
### 2. Scanner State Persistence & Crash Recovery
|
|
||||||
- **Resilient State**: The scan loop continuously persists progress to the database (`Settings` table), recording `CurrentScanningType` and `CurrentScanningPage`.
|
|
||||||
- **Exit Recovery**: Resumes exactly from the last saved page and asset type upon restart.
|
|
||||||
|
|
||||||
### 3. Stealth Scheduling with Randomized Jitter
|
|
||||||
- **Dynamic Delays**: Scanner wait times are read dynamically from database configuration (`Settings`).
|
|
||||||
- **Jitter Offset**: Randomized offsets between batches and asset types to prevent rate limiting.
|
|
||||||
|
|
||||||
### 4. Active vs. Dead Asset Tracking
|
|
||||||
- **Recency Filter**: Assets updated within 14 days are active. Inactive assets are flagged.
|
|
||||||
|
|
||||||
### 5. MQTT RPC Interface
|
|
||||||
- **Get Asset by ISIN**: Subscribes to `services/request/assets_Get/#`.
|
|
||||||
- **Omnibox Search**: Subscribes to `services/request/assets_Search/#`.
|
|
||||||
- **JIT Fallback**: Performs JIT-lookup directly against the Trade Republic API for unknown ISINs.
|
|
||||||
|
|
||||||
### 6. Local Indexing File (`index.json`)
|
|
||||||
- **Automatic Regeneration**: Serializes active assets list (`ISIN`, `Name`) to shared volume `assets/index/index.json`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Feature Status
|
|
||||||
|
|
||||||
### Implemented Features
|
|
||||||
- [x] Trade Republic WebSocket scanner for Stocks, Crypto, Derivatives, Bonds, ETFs.
|
|
||||||
- [x] State persistence & crash recovery in PostgreSQL.
|
|
||||||
- [x] JIT Trade Republic lookup fallback for new ISINs.
|
|
||||||
- [x] Shared local index file generation (`index.json`).
|
|
||||||
- [x] Zero-Allocation MQTT RPC handlers.
|
|
||||||
- [x] Pure Worker Service architecture (No Kestrel HTTP server).
|
|
||||||
|
|
||||||
### Planned Features
|
|
||||||
- [ ] Real-time WebSocket price tick streaming over MQTT topic `finlytic/assets/ticks/{isin}`.
|
|
||||||
- [ ] Derivative option-chain Greeks calculator integration.
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FinlyticAssets.Models;
|
|
||||||
using FinlyticAssets.Util;
|
using FinlyticAssets.Util;
|
||||||
using FinlyticCore.Dtos.TradeRepublic;
|
using FinlyticCore.Dtos.TradeRepublic;
|
||||||
using FinlyticCore.Models.Assets;
|
using FinlyticCore.Models.Assets;
|
||||||
@@ -15,14 +15,16 @@ using Microsoft.Extensions.Hosting;
|
|||||||
namespace FinlyticAssets.Services;
|
namespace FinlyticAssets.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A background service that runs a continuous asset synchronization loop,
|
/// A background service that runs a continuous asset synchronization loop for Stocks and ETFs,
|
||||||
/// scanning Trade Republic to retrieve, update, and index all supported asset types.
|
/// retrieving, updating, and indexing assets while downloading logos inline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AssetScannerBackgroundService : BackgroundService
|
public class AssetScannerBackgroundService : BackgroundService
|
||||||
{
|
{
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
private readonly IFinlyticLogger<AssetScannerBackgroundService> _finlyticLogger;
|
private readonly IFinlyticLogger<AssetScannerBackgroundService> _finlyticLogger;
|
||||||
|
|
||||||
|
private static readonly AssetType[] ScannedAssetTypes = [AssetType.Stock, AssetType.Fund];
|
||||||
|
|
||||||
private AssetsCount? _assetsCount;
|
private AssetsCount? _assetsCount;
|
||||||
private AssetsCount? _currAssetsCount;
|
private AssetsCount? _currAssetsCount;
|
||||||
|
|
||||||
@@ -54,24 +56,35 @@ public class AssetScannerBackgroundService : BackgroundService
|
|||||||
{
|
{
|
||||||
using var scope = _serviceScopeFactory.CreateScope();
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
var tradeRepublicService = scope.ServiceProvider.GetRequiredService<ITradeRepublicService>();
|
var tradeRepublicService = scope.ServiceProvider.GetRequiredService<ITradeRepublicService>();
|
||||||
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
|
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
||||||
var assetsDbService = scope.ServiceProvider.GetRequiredService<IAssetsDbService>();
|
var assetsDbService = scope.ServiceProvider.GetRequiredService<IAssetsDbService>();
|
||||||
var indexService = scope.ServiceProvider.GetRequiredService<IAssetsIndexService>();
|
var indexService = scope.ServiceProvider.GetRequiredService<IAssetsIndexService>();
|
||||||
|
|
||||||
|
var isAutoScanEnabled = await settingsService.GetSettingAsync(SettingKeys.ScannerEnableAutoScan, stoppingToken);
|
||||||
|
if (!isAutoScanEnabled)
|
||||||
|
{
|
||||||
|
await _finlyticLogger.LogDebugAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Auto scan is disabled in dynamic settings. Waiting 1 minute...");
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Requesting total asset counts from Trade Republic...");
|
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Requesting total asset counts from Trade Republic...");
|
||||||
_assetsCount = await tradeRepublicService.GetAssetsCount(stoppingToken);
|
_assetsCount = await tradeRepublicService.GetAssetsCount(stoppingToken);
|
||||||
_currAssetsCount = new AssetsCount();
|
_currAssetsCount = new AssetsCount();
|
||||||
|
|
||||||
var initSettings = await settingsService.GetSettings();
|
var initCurrentTypeStr = await settingsService.GetSettingAsync(SettingKeys.ScannerCurrentScanningType, stoppingToken);
|
||||||
var isRecoveryMode = initSettings.CurrentScanningPage > 0;
|
var initCurrentPage = await settingsService.GetSettingAsync(SettingKeys.ScannerCurrentScanningPage, stoppingToken);
|
||||||
|
var isRecoveryMode = initCurrentPage > 0;
|
||||||
|
|
||||||
foreach (var type in Enum.GetValues<AssetType>())
|
Enum.TryParse<AssetType>(initCurrentTypeStr, true, out var initCurrentType);
|
||||||
|
|
||||||
|
foreach (var type in ScannedAssetTypes)
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
if (stoppingToken.IsCancellationRequested) break;
|
||||||
|
|
||||||
if (isRecoveryMode)
|
if (isRecoveryMode)
|
||||||
{
|
{
|
||||||
if (type != initSettings.CurrentScanningType)
|
if (type != initCurrentType)
|
||||||
{
|
{
|
||||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Recovery: {AssetType} was already processed. Skipping.", type);
|
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Recovery: {AssetType} was already processed. Skipping.", type);
|
||||||
continue;
|
continue;
|
||||||
@@ -80,19 +93,17 @@ public class AssetScannerBackgroundService : BackgroundService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var settings = await settingsService.GetSettings();
|
await settingsService.SetSettingAsync(SettingKeys.ScannerCurrentScanningType, type.ToString(), stoppingToken);
|
||||||
settings.CurrentScanningType = type;
|
await settingsService.SetSettingAsync(SettingKeys.ScannerCurrentScanningPage, 0, stoppingToken);
|
||||||
settings.CurrentScanningPage = 0;
|
|
||||||
await settingsService.SaveSettings(settings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Processing asset type: {AssetType}...", type);
|
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Processing asset type: {AssetType}...", type);
|
||||||
await HandleAssetType(type, tradeRepublicService, settingsService, assetsDbService, indexService, stoppingToken);
|
await HandleAssetType(type, tradeRepublicService, settingsService, assetsDbService, indexService, stoppingToken);
|
||||||
|
|
||||||
var currentSettings = await settingsService.GetSettings();
|
var finishedInitial = await settingsService.GetSettingAsync(SettingKeys.ScannerFinishedInitialScan, stoppingToken);
|
||||||
var delaySeconds = currentSettings.FinishedInitialScan
|
var delaySeconds = finishedInitial
|
||||||
? currentSettings.AssetUpdateTypeDelay
|
? await settingsService.GetSettingAsync(SettingKeys.ScannerTypeDelay, stoppingToken)
|
||||||
: currentSettings.InitAssetUpdateTypeDelay;
|
: await settingsService.GetSettingAsync(SettingKeys.ScannerInitTypeDelay, stoppingToken);
|
||||||
|
|
||||||
if (delaySeconds > 0)
|
if (delaySeconds > 0)
|
||||||
{
|
{
|
||||||
@@ -102,19 +113,18 @@ public class AssetScannerBackgroundService : BackgroundService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var finalSettings = await settingsService.GetSettings();
|
await settingsService.SetSettingAsync(SettingKeys.ScannerCurrentScanningPage, 0, stoppingToken);
|
||||||
finalSettings.CurrentScanningPage = 0;
|
|
||||||
|
|
||||||
if (!finalSettings.FinishedInitialScan && !stoppingToken.IsCancellationRequested)
|
var finishedScan = await settingsService.GetSettingAsync(SettingKeys.ScannerFinishedInitialScan, stoppingToken);
|
||||||
|
if (!finishedScan && !stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Initial scan successfully completed. Switching FinishedInitialScan to true.");
|
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Initial scan successfully completed. Switching FinishedInitialScan to true.");
|
||||||
finalSettings.FinishedInitialScan = true;
|
await settingsService.SetSettingAsync(SettingKeys.ScannerFinishedInitialScan, true, stoppingToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
await settingsService.SaveSettings(finalSettings);
|
var cycleDelay = await settingsService.GetSettingAsync(SettingKeys.ScannerCycleDelayMinutes, stoppingToken);
|
||||||
|
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Full scan cycle completed. Waiting {Minutes} minutes before starting the next cycle.", cycleDelay);
|
||||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Full scan cycle completed. Waiting 1 minute before starting the next cycle.");
|
await Task.Delay(TimeSpan.FromMinutes(cycleDelay), stoppingToken);
|
||||||
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
|
||||||
}
|
}
|
||||||
catch (Exception e) when (!stoppingToken.IsCancellationRequested)
|
catch (Exception e) when (!stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@@ -129,7 +139,7 @@ public class AssetScannerBackgroundService : BackgroundService
|
|||||||
private async Task HandleAssetType(
|
private async Task HandleAssetType(
|
||||||
AssetType type,
|
AssetType type,
|
||||||
ITradeRepublicService tradeRepublicService,
|
ITradeRepublicService tradeRepublicService,
|
||||||
ISettingsDbService settingsDbService,
|
ISettingsService settingsService,
|
||||||
IAssetsDbService assetsDbService,
|
IAssetsDbService assetsDbService,
|
||||||
IAssetsIndexService indexService,
|
IAssetsIndexService indexService,
|
||||||
CancellationToken stoppingToken)
|
CancellationToken stoppingToken)
|
||||||
@@ -144,26 +154,28 @@ public class AssetScannerBackgroundService : BackgroundService
|
|||||||
_currAssetsCount ??= new AssetsCount();
|
_currAssetsCount ??= new AssetsCount();
|
||||||
var currentItemOffset = 0;
|
var currentItemOffset = 0;
|
||||||
|
|
||||||
var settings = await settingsDbService.GetSettings();
|
var configuredPageSize = await settingsService.GetSettingAsync(SettingKeys.ScannerMaxPageSize, stoppingToken);
|
||||||
var pageSize = Math.Clamp(settings.TradeRepublicMaxRequestPageSize <= 0 ? 50 : settings.TradeRepublicMaxRequestPageSize, 1, 100);
|
var pageSize = Math.Clamp(configuredPageSize <= 0 ? 50 : configuredPageSize, 1, 100);
|
||||||
|
|
||||||
if (settings.CurrentScanningType == type && settings.CurrentScanningPage > 0)
|
var currentScanningTypeStr = await settingsService.GetSettingAsync(SettingKeys.ScannerCurrentScanningType, stoppingToken);
|
||||||
|
var currentScanningPage = await settingsService.GetSettingAsync(SettingKeys.ScannerCurrentScanningPage, stoppingToken);
|
||||||
|
|
||||||
|
if (string.Equals(currentScanningTypeStr, type.ToString(), StringComparison.OrdinalIgnoreCase) && currentScanningPage > 0)
|
||||||
{
|
{
|
||||||
currentItemOffset = (settings.CurrentScanningPage - 1) * pageSize;
|
currentItemOffset = (currentScanningPage - 1) * pageSize;
|
||||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Resuming full scan for {AssetType} from Page {Page} (Calculated Offset: {Offset}).",
|
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Resuming full scan for {AssetType} from Page {Page} (Calculated Offset: {Offset}).",
|
||||||
type, settings.CurrentScanningPage, currentItemOffset);
|
type, currentScanningPage, currentItemOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (currentItemOffset < totalCount && !stoppingToken.IsCancellationRequested)
|
while (currentItemOffset < totalCount && !stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
var currentSettings = await settingsDbService.GetSettings();
|
configuredPageSize = await settingsService.GetSettingAsync(SettingKeys.ScannerMaxPageSize, stoppingToken);
|
||||||
pageSize = Math.Clamp(currentSettings.TradeRepublicMaxRequestPageSize <= 0 ? 50 : currentSettings.TradeRepublicMaxRequestPageSize, 1, 100);
|
pageSize = Math.Clamp(configuredPageSize <= 0 ? 50 : configuredPageSize, 1, 100);
|
||||||
|
|
||||||
var currentPage = (currentItemOffset / pageSize) + 1;
|
var currentPage = (currentItemOffset / pageSize) + 1;
|
||||||
|
|
||||||
currentSettings.CurrentScanningType = type;
|
await settingsService.SetSettingAsync(SettingKeys.ScannerCurrentScanningType, type.ToString(), stoppingToken);
|
||||||
currentSettings.CurrentScanningPage = currentPage;
|
await settingsService.SetSettingAsync(SettingKeys.ScannerCurrentScanningPage, currentPage, stoppingToken);
|
||||||
await settingsDbService.SaveSettings(currentSettings);
|
|
||||||
|
|
||||||
await _finlyticLogger.LogDebugAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Fetching {AssetType} - Page {Page}. Numerical Offset: {Offset}/{Total}",
|
await _finlyticLogger.LogDebugAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Fetching {AssetType} - Page {Page}. Numerical Offset: {Offset}/{Total}",
|
||||||
type, currentPage, currentItemOffset, totalCount);
|
type, currentPage, currentItemOffset, totalCount);
|
||||||
@@ -173,8 +185,7 @@ public class AssetScannerBackgroundService : BackgroundService
|
|||||||
if (assets?.Results == null || assets.Results.Count == 0)
|
if (assets?.Results == null || assets.Results.Count == 0)
|
||||||
{
|
{
|
||||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Fetch for {AssetType} (Page {Page}) returned no results. Reached end of available assets.", type, currentPage);
|
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Fetch for {AssetType} (Page {Page}) returned no results. Reached end of available assets.", type, currentPage);
|
||||||
currentSettings.CurrentScanningPage = 0;
|
await settingsService.SetSettingAsync(SettingKeys.ScannerCurrentScanningPage, 0, stoppingToken);
|
||||||
await settingsDbService.SaveSettings(currentSettings);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,14 +197,14 @@ public class AssetScannerBackgroundService : BackgroundService
|
|||||||
if (assets.Results.Count < pageSize)
|
if (assets.Results.Count < pageSize)
|
||||||
{
|
{
|
||||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Reached the last page for {AssetType}.", type);
|
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Reached the last page for {AssetType}.", type);
|
||||||
currentSettings.CurrentScanningPage = 0;
|
await settingsService.SetSettingAsync(SettingKeys.ScannerCurrentScanningPage, 0, stoppingToken);
|
||||||
await settingsDbService.SaveSettings(currentSettings);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var delaySeconds = currentSettings.FinishedInitialScan
|
var finishedInitial = await settingsService.GetSettingAsync(SettingKeys.ScannerFinishedInitialScan, stoppingToken);
|
||||||
? currentSettings.BatchAssetUpdateDelay
|
var delaySeconds = finishedInitial
|
||||||
: currentSettings.InitBatchAssetUpdateDelay;
|
? await settingsService.GetSettingAsync(SettingKeys.ScannerBatchDelay, stoppingToken)
|
||||||
|
: await settingsService.GetSettingAsync(SettingKeys.ScannerInitBatchDelay, stoppingToken);
|
||||||
|
|
||||||
if (delaySeconds > 0)
|
if (delaySeconds > 0)
|
||||||
{
|
{
|
||||||
@@ -219,6 +230,26 @@ public class AssetScannerBackgroundService : BackgroundService
|
|||||||
var changedRows = await assetsDbService.AddOrUpdateAssetsAsync(assets);
|
var changedRows = await assetsDbService.AddOrUpdateAssetsAsync(assets);
|
||||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] [Scan] {Count} assets passed to the DB service. {Changed} modifications/inserts executed.", assets.Count, changedRows);
|
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] [Scan] {Count} assets passed to the DB service. {Changed} modifications/inserts executed.", assets.Count, changedRows);
|
||||||
|
|
||||||
|
// Inline logo fetching for each asset in the batch if missing on disk
|
||||||
|
string directoryPath = Volumes.LogosRelativePath;
|
||||||
|
foreach (var a in assets)
|
||||||
|
{
|
||||||
|
if (stoppingToken.IsCancellationRequested) break;
|
||||||
|
if (string.IsNullOrWhiteSpace(a.Isin)) continue;
|
||||||
|
|
||||||
|
string cleanIsin = a.Isin.Trim().ToUpperInvariant();
|
||||||
|
string logoPath = Path.Combine(directoryPath, $"{cleanIsin}.svg");
|
||||||
|
|
||||||
|
if (!File.Exists(logoPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await indexService.DownloadAndSaveLogoAsync(cleanIsin, stoppingToken);
|
||||||
|
}
|
||||||
|
catch { /* Ignore non-fatal logo fetch failures */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (changedRows > 0)
|
if (changedRows > 0)
|
||||||
{
|
{
|
||||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Database modifications detected. Recreating the asset index file...");
|
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Database modifications detected. Recreating the asset index file...");
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
namespace FinlyticAssets.Services;
|
namespace FinlyticAssets.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines database operations for managing Trade Republic asset entities.
|
/// Defines database operations for managing Trade Republic asset entities and on-demand derivatives.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IAssetsDbService
|
public interface IAssetsDbService
|
||||||
{
|
{
|
||||||
@@ -24,7 +24,6 @@ public interface IAssetsDbService
|
|||||||
public Task<List<AssetEntity>> GetAssetsByIsinAsync(string isin);
|
public Task<List<AssetEntity>> GetAssetsByIsinAsync(string isin);
|
||||||
public Task<List<AssetEntity>> GetValidAssetsByIsinAsync(string isin);
|
public Task<List<AssetEntity>> GetValidAssetsByIsinAsync(string isin);
|
||||||
public Task<List<AssetEntity>> FindAffectedActiveAssetsAsync(string searchQuery);
|
public Task<List<AssetEntity>> FindAffectedActiveAssetsAsync(string searchQuery);
|
||||||
public Task UpdateAssetImageIdAsync(string isin, string imageId);
|
|
||||||
public Task<bool> DeleteAssetAsync(string isin);
|
public Task<bool> DeleteAssetAsync(string isin);
|
||||||
public Task<List<AssetEntity>> GetDiscoveryAssetsAsync(int limit = 15);
|
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);
|
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,
|
Asset = a,
|
||||||
Score = (a.Tags?.Count ?? 0) * 10
|
Score = (a.Tags?.Count ?? 0) * 10
|
||||||
+ (a.HasCfd ? 5 : 0)
|
+ (a.HasCfd ? 5 : 0)
|
||||||
+ (string.IsNullOrEmpty(a.ImageId) ? 0 : 15)
|
|
||||||
+ (a.Name.Length > 3 ? 5 : 0)
|
+ (a.Name.Length > 3 ? 5 : 0)
|
||||||
})
|
})
|
||||||
.OrderByDescending(x => x.Score)
|
.OrderByDescending(x => x.Score)
|
||||||
@@ -147,7 +145,6 @@ public class AssetsDbService : IAssetsDbService
|
|||||||
existingEntity.Type = dtoAsset.Type;
|
existingEntity.Type = dtoAsset.Type;
|
||||||
existingEntity.InstrumentCategory = dtoAsset.InstrumentCategory;
|
existingEntity.InstrumentCategory = dtoAsset.InstrumentCategory;
|
||||||
existingEntity.HasCfd = dtoAsset.HasCfd;
|
existingEntity.HasCfd = dtoAsset.HasCfd;
|
||||||
existingEntity.ImageId = dtoAsset.ImageId;
|
|
||||||
existingEntity.LastUpdatedAt = now;
|
existingEntity.LastUpdatedAt = now;
|
||||||
|
|
||||||
UpdateSubtypeProperties(existingEntity, dtoAsset);
|
UpdateSubtypeProperties(existingEntity, dtoAsset);
|
||||||
@@ -220,14 +217,13 @@ public class AssetsDbService : IAssetsDbService
|
|||||||
existingEntity.Type != dto.Type ||
|
existingEntity.Type != dto.Type ||
|
||||||
existingEntity.InstrumentCategory != dto.InstrumentCategory ||
|
existingEntity.InstrumentCategory != dto.InstrumentCategory ||
|
||||||
existingEntity.HasCfd != dto.HasCfd ||
|
existingEntity.HasCfd != dto.HasCfd ||
|
||||||
existingEntity.ImageId != dto.ImageId ||
|
!existingEntity.Tags.Select(t => t.Id).Order().SequenceEqual(mappedTags.Select(t => t.Id).Order()) ||
|
||||||
!existingEntity.Tags.SequenceEqual(mappedTags))
|
HasSubtypeChanges(existingEntity, dto))
|
||||||
{
|
{
|
||||||
existingEntity.Name = dto.Name;
|
existingEntity.Name = dto.Name;
|
||||||
existingEntity.Type = dto.Type;
|
existingEntity.Type = dto.Type;
|
||||||
existingEntity.InstrumentCategory = dto.InstrumentCategory;
|
existingEntity.InstrumentCategory = dto.InstrumentCategory;
|
||||||
existingEntity.HasCfd = dto.HasCfd;
|
existingEntity.HasCfd = dto.HasCfd;
|
||||||
existingEntity.ImageId = dto.ImageId;
|
|
||||||
existingEntity.LastUpdatedAt = now;
|
existingEntity.LastUpdatedAt = now;
|
||||||
existingEntity.Tags = mappedTags;
|
existingEntity.Tags = mappedTags;
|
||||||
|
|
||||||
@@ -255,18 +251,8 @@ public class AssetsDbService : IAssetsDbService
|
|||||||
Type = stock.Type,
|
Type = stock.Type,
|
||||||
InstrumentCategory = stock.InstrumentCategory,
|
InstrumentCategory = stock.InstrumentCategory,
|
||||||
HasCfd = stock.HasCfd,
|
HasCfd = stock.HasCfd,
|
||||||
ImageId = stock.ImageId,
|
|
||||||
DerivativeProductCategories = stock.DerivativeProductCategories?.ToList() ?? new List<string>()
|
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
|
TradeRepublicEtf etf => new EtfEntity
|
||||||
{
|
{
|
||||||
Isin = etf.Isin,
|
Isin = etf.Isin,
|
||||||
@@ -274,7 +260,6 @@ public class AssetsDbService : IAssetsDbService
|
|||||||
Type = etf.Type,
|
Type = etf.Type,
|
||||||
InstrumentCategory = etf.InstrumentCategory,
|
InstrumentCategory = etf.InstrumentCategory,
|
||||||
HasCfd = etf.HasCfd,
|
HasCfd = etf.HasCfd,
|
||||||
ImageId = etf.ImageId,
|
|
||||||
DerivativeProductCategories = etf.DerivativeProductCategories?.ToList() ?? new List<string>()
|
DerivativeProductCategories = etf.DerivativeProductCategories?.ToList() ?? new List<string>()
|
||||||
},
|
},
|
||||||
TradeRepublicSynthetic syn => new SyntheticEntity
|
TradeRepublicSynthetic syn => new SyntheticEntity
|
||||||
@@ -284,39 +269,15 @@ public class AssetsDbService : IAssetsDbService
|
|||||||
Type = syn.Type,
|
Type = syn.Type,
|
||||||
InstrumentCategory = syn.InstrumentCategory,
|
InstrumentCategory = syn.InstrumentCategory,
|
||||||
HasCfd = syn.HasCfd,
|
HasCfd = syn.HasCfd,
|
||||||
ImageId = syn.ImageId,
|
|
||||||
DerivativeProductCategories = syn.DerivativeProductCategories?.ToList() ?? new List<string>()
|
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
|
_ => new StockEntity
|
||||||
{
|
{
|
||||||
Isin = dto.Isin,
|
Isin = dto.Isin,
|
||||||
Name = dto.Name,
|
Name = dto.Name,
|
||||||
Type = dto.Type,
|
Type = dto.Type,
|
||||||
InstrumentCategory = dto.InstrumentCategory,
|
InstrumentCategory = dto.InstrumentCategory,
|
||||||
HasCfd = dto.HasCfd,
|
HasCfd = dto.HasCfd
|
||||||
ImageId = dto.ImageId
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -334,14 +295,21 @@ public class AssetsDbService : IAssetsDbService
|
|||||||
case SyntheticEntity syn when dto is TradeRepublicSynthetic synDto:
|
case SyntheticEntity syn when dto is TradeRepublicSynthetic synDto:
|
||||||
syn.DerivativeProductCategories = synDto.DerivativeProductCategories?.ToList() ?? new List<string>();
|
syn.DerivativeProductCategories = synDto.DerivativeProductCategories?.ToList() ?? new List<string>();
|
||||||
break;
|
break;
|
||||||
case BondEntity bond when dto is TradeRepublicBond b:
|
}
|
||||||
bond.BondIssuerName = b.BondIssuerName;
|
}
|
||||||
bond.SearchSubtitle = b.SearchSubtitle;
|
|
||||||
break;
|
private static bool HasSubtypeChanges(AssetEntity entity, TradeRepublicAsset dto)
|
||||||
case DerivativeEntity deriv when dto is TradeRepublicDerivative d:
|
{
|
||||||
deriv.UnderlyingIsin = d.UnderlyingIsin;
|
switch (entity)
|
||||||
deriv.DerivativeProductCategories = d.DerivativeProductCategories?.ToList() ?? new List<string>();
|
{
|
||||||
break;
|
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();
|
.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>
|
/// <summary>Inherits documentation from interface.</summary>
|
||||||
public async Task<bool> DeleteAssetAsync(string isin)
|
public async Task<bool> DeleteAssetAsync(string isin)
|
||||||
{
|
{
|
||||||
@@ -412,7 +362,6 @@ public class AssetsDbService : IAssetsDbService
|
|||||||
int pageIndex = Math.Max(0, page ?? 0);
|
int pageIndex = Math.Max(0, page ?? 0);
|
||||||
|
|
||||||
decimal levQuery = targetLeverage.HasValue && targetLeverage.Value > 0 ? targetLeverage.Value : 0m;
|
decimal levQuery = targetLeverage.HasValue && targetLeverage.Value > 0 ? targetLeverage.Value : 0m;
|
||||||
|
|
||||||
string trAfter = !string.IsNullOrEmpty(after) ? after : (pageIndex > 0 ? pageIndex.ToString() : "0");
|
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})",
|
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 now = DateTime.UtcNow;
|
||||||
var isins = fetchedItems.Select(r => r.Isin).ToList();
|
var isins = fetchedItems.Select(r => r.Isin).ToList();
|
||||||
var existingDerivatives = await _context.TradeRepublicAssets
|
var existingDerivatives = await _context.Derivatives
|
||||||
.OfType<DerivativeEntity>()
|
|
||||||
.Where(d => isins.Contains(d.Isin))
|
.Where(d => isins.Contains(d.Isin))
|
||||||
.ToDictionaryAsync(d => d.Isin, cancellationToken);
|
.ToDictionaryAsync(d => d.Isin, cancellationToken);
|
||||||
|
|
||||||
@@ -466,14 +414,13 @@ public class AssetsDbService : IAssetsDbService
|
|||||||
existing.NextGenProductCategoryName = item.NextGenProductCategoryName;
|
existing.NextGenProductCategoryName = item.NextGenProductCategoryName;
|
||||||
existing.Issuer = item.Issuer;
|
existing.Issuer = item.Issuer;
|
||||||
existing.IssuerDisplayName = item.IssuerDisplayName;
|
existing.IssuerDisplayName = item.IssuerDisplayName;
|
||||||
existing.IssuerImageId = item.IssuerImageId;
|
|
||||||
existing.Size = item.Size;
|
existing.Size = item.Size;
|
||||||
existing.Factor = item.Factor;
|
existing.Factor = item.Factor;
|
||||||
existing.Delta = item.Delta;
|
existing.Delta = item.Delta;
|
||||||
existing.Currency = item.Currency;
|
existing.Currency = item.Currency;
|
||||||
existing.LastUpdatedAt = now;
|
existing.LastUpdatedAt = now;
|
||||||
|
|
||||||
_context.TradeRepublicAssets.Update(existing);
|
_context.Derivatives.Update(existing);
|
||||||
resultEntities.Add(existing);
|
resultEntities.Add(existing);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -482,8 +429,6 @@ public class AssetsDbService : IAssetsDbService
|
|||||||
{
|
{
|
||||||
Isin = item.Isin,
|
Isin = item.Isin,
|
||||||
Name = !string.IsNullOrWhiteSpace(item.ProductCategoryName) ? item.ProductCategoryName : item.Isin,
|
Name = !string.IsNullOrWhiteSpace(item.ProductCategoryName) ? item.ProductCategoryName : item.Isin,
|
||||||
Type = "derivative",
|
|
||||||
InstrumentCategory = "derivative",
|
|
||||||
UnderlyingIsin = underlyingIsin,
|
UnderlyingIsin = underlyingIsin,
|
||||||
Strike = item.Strike ?? 0m,
|
Strike = item.Strike ?? 0m,
|
||||||
Barrier = item.Barrier ?? 0m,
|
Barrier = item.Barrier ?? 0m,
|
||||||
@@ -494,27 +439,33 @@ public class AssetsDbService : IAssetsDbService
|
|||||||
NextGenProductCategoryName = item.NextGenProductCategoryName,
|
NextGenProductCategoryName = item.NextGenProductCategoryName,
|
||||||
Issuer = item.Issuer,
|
Issuer = item.Issuer,
|
||||||
IssuerDisplayName = item.IssuerDisplayName,
|
IssuerDisplayName = item.IssuerDisplayName,
|
||||||
IssuerImageId = item.IssuerImageId,
|
|
||||||
Size = item.Size,
|
Size = item.Size,
|
||||||
Factor = item.Factor,
|
Factor = item.Factor,
|
||||||
Delta = item.Delta,
|
Delta = item.Delta,
|
||||||
Currency = item.Currency,
|
Currency = item.Currency,
|
||||||
LastUpdatedAt = now
|
LastUpdatedAt = now,
|
||||||
|
CreatedAt = now
|
||||||
};
|
};
|
||||||
|
|
||||||
await _context.TradeRepublicAssets.AddAsync(newDeriv, cancellationToken);
|
await _context.Derivatives.AddAsync(newDeriv, cancellationToken);
|
||||||
resultEntities.Add(newDeriv);
|
resultEntities.Add(newDeriv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
return resultEntities;
|
return resultEntities.Where(d => d.Barrier > 0 && d.Leverage > 0).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await _context.TradeRepublicAssets
|
var dbQuery = _context.Derivatives
|
||||||
.OfType<DerivativeEntity>()
|
|
||||||
.AsNoTracking()
|
.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)
|
.Take(pageSize)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using System.Net.Http;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FinlyticAssets.Models;
|
using FinlyticCore.Models.Assets;
|
||||||
using FinlyticAssets.Util;
|
using FinlyticAssets.Util;
|
||||||
using FinlyticCore.Services;
|
using FinlyticCore.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using FinlyticAssets.Util;
|
|
||||||
using FinlyticCore.Services;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
|
|
||||||
namespace FinlyticAssets.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dedicated background service that periodically scans for missing asset logos in the local storage directory
|
|
||||||
/// and fetches them in batches from Trade Republic CDN.
|
|
||||||
/// Swaps missing/404 logos with a clean SVG placeholder image and triggers ReCreateIndexFileAsync.
|
|
||||||
/// </summary>
|
|
||||||
public class LogoFetcherBackgroundService : BackgroundService
|
|
||||||
{
|
|
||||||
private readonly IFinlyticLogger<LogoFetcherBackgroundService> _finlyticLogger;
|
|
||||||
private readonly IServiceScopeFactory _scopeFactory;
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
|
|
||||||
private const string PlaceholderSvg = """
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
|
||||||
<rect width="100" height="100" rx="30" fill="#1E293B"/>
|
|
||||||
<path d="M 30 65 L 45 45 L 60 55 L 75 35" fill="none" stroke="#10B981" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<circle cx="75" cy="35" r="5" fill="#06B6D4"/>
|
|
||||||
</svg>
|
|
||||||
""";
|
|
||||||
|
|
||||||
public LogoFetcherBackgroundService(
|
|
||||||
IFinlyticLogger<LogoFetcherBackgroundService> finlyticLogger,
|
|
||||||
IServiceScopeFactory scopeFactory)
|
|
||||||
{
|
|
||||||
_finlyticLogger = finlyticLogger;
|
|
||||||
_scopeFactory = scopeFactory;
|
|
||||||
_httpClient = new HttpClient();
|
|
||||||
_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
|
||||||
_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "image/svg+xml,image/*,*/*");
|
|
||||||
_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Referer", "https://traderepublic.com/");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
||||||
{
|
|
||||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[LogoFetcherBackgroundService] LogoFetcherBackgroundService started. Will fetch missing logos periodically.");
|
|
||||||
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
|
|
||||||
|
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await ProcessMissingLogosBatchAsync(stoppingToken);
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
await _finlyticLogger.LogErrorAsync(SettingKeys.AssetsChannel, ex, "[LogoFetcherBackgroundService] Error occurred while executing logo batch fetch.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[LogoFetcherBackgroundService] LogoFetcherBackgroundService stopped.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ProcessMissingLogosBatchAsync(CancellationToken stoppingToken)
|
|
||||||
{
|
|
||||||
using var scope = _scopeFactory.CreateScope();
|
|
||||||
var dbService = scope.ServiceProvider.GetRequiredService<IAssetsDbService>();
|
|
||||||
var indexService = scope.ServiceProvider.GetRequiredService<IAssetsIndexService>();
|
|
||||||
|
|
||||||
var validAssets = await dbService.GetAllValidAssetsAsync();
|
|
||||||
if (validAssets == null || !validAssets.Any()) return;
|
|
||||||
|
|
||||||
string directoryPath = Volumes.LogosRelativePath;
|
|
||||||
if (!Directory.Exists(directoryPath))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(directoryPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
var missingIsins = validAssets
|
|
||||||
.Select(a => a.Isin?.Trim().ToUpperInvariant())
|
|
||||||
.Where(isin => !string.IsNullOrEmpty(isin))
|
|
||||||
.Distinct()
|
|
||||||
.Where(isin => !File.Exists(Path.Combine(directoryPath, $"{isin}.svg")))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (missingIsins.Count == 0)
|
|
||||||
{
|
|
||||||
await _finlyticLogger.LogDebugAsync(SettingKeys.AssetsChannel, "[LogoFetcherBackgroundService] All asset logos are downloaded and up to date.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var batchToFetch = missingIsins.Take(60).ToList();
|
|
||||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[LogoFetcherBackgroundService] Found {Count} missing logos on disk. Fetching bulk batch of {BatchSize} logos...", missingIsins.Count, batchToFetch.Count);
|
|
||||||
|
|
||||||
int successCount = 0;
|
|
||||||
|
|
||||||
foreach (var isin in batchToFetch)
|
|
||||||
{
|
|
||||||
if (stoppingToken.IsCancellationRequested) break;
|
|
||||||
|
|
||||||
string targetUrl = $"https://assets.traderepublic.com/img/logos/{isin}/v2/dark.min.svg";
|
|
||||||
string filePath = Path.Combine(directoryPath, $"{isin}.svg");
|
|
||||||
string dbImageEndpoint = $"/api/v1/logo/{isin}";
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var response = await _httpClient.GetAsync(targetUrl, stoppingToken);
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
byte[] data = await response.Content.ReadAsByteArrayAsync(stoppingToken);
|
|
||||||
await File.WriteAllBytesAsync(filePath, data, stoppingToken);
|
|
||||||
successCount++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _finlyticLogger.LogWarningAsync(SettingKeys.AssetsChannel, "[LogoFetcherBackgroundService] Logo not found on CDN for ISIN {Isin} (HTTP {StatusCode}). Saving SVG placeholder.", isin, response.StatusCode);
|
|
||||||
byte[] placeholderData = Encoding.UTF8.GetBytes(PlaceholderSvg);
|
|
||||||
await File.WriteAllBytesAsync(filePath, placeholderData, stoppingToken);
|
|
||||||
successCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
await dbService.UpdateAssetImageIdAsync(isin, dbImageEndpoint);
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
await _finlyticLogger.LogWarningAsync(SettingKeys.AssetsChannel, ex, "[LogoFetcherBackgroundService] Exception while downloading logo for ISIN {Isin} from {Url}. Saving SVG placeholder.", isin, targetUrl);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
byte[] placeholderData = Encoding.UTF8.GetBytes(PlaceholderSvg);
|
|
||||||
await File.WriteAllBytesAsync(filePath, placeholderData, stoppingToken);
|
|
||||||
successCount++;
|
|
||||||
|
|
||||||
await dbService.UpdateAssetImageIdAsync(isin, dbImageEndpoint);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(50, stoppingToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[LogoFetcherBackgroundService] Batch fetch complete. Successfully processed {SuccessCount}/{BatchSize} logos. Remaining missing: {Remaining}",
|
|
||||||
successCount, batchToFetch.Count, missingIsins.Count - batchToFetch.Count);
|
|
||||||
|
|
||||||
if (successCount > 0)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await indexService.ReCreateIndexFileAsync(stoppingToken);
|
|
||||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[LogoFetcherBackgroundService] Successfully updated index.json after logo batch fetch.");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _finlyticLogger.LogWarningAsync(SettingKeys.AssetsChannel, ex, "[LogoFetcherBackgroundService] Failed to update index.json after logo batch fetch.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
using FinlyticAssets.Database;
|
|
||||||
using FinlyticAssets.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace FinlyticAssets.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Defines the business logic for managing global application settings.
|
|
||||||
/// Supports retrieving and updating (upserting) the central single-row configuration record.
|
|
||||||
/// </summary>
|
|
||||||
public interface ISettingsDbService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves the current global settings from the database.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>
|
|
||||||
/// A task that represents the asynchronous operation. The task result contains the current <see cref="Settings"/>.
|
|
||||||
/// If no settings exist in the database yet, a new instance initialized with default values is returned.
|
|
||||||
/// </returns>
|
|
||||||
public Task<Settings> GetSettings();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Persists the provided settings by updating the existing record or inserting the first one if the table is empty.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="settings">The new configuration values to be persisted.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// A task that represents the asynchronous operation. The task result contains the freshly saved
|
|
||||||
/// and tracked <see cref="Settings"/> instance.
|
|
||||||
/// </returns>
|
|
||||||
public Task<Settings> SaveSettings(Settings settings);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates settings from a key-value dictionary received via Admin Panel MQTT events.
|
|
||||||
/// </summary>
|
|
||||||
public Task UpdateSettingsFromDictionary(Dictionary<string, string> dictionary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implements the <see cref="ISettingsDbService"/> utilizing Entity Framework Core.
|
|
||||||
/// This service is designed for a single-row table architecture to maintain stateful global configurations.
|
|
||||||
/// </summary>
|
|
||||||
public class SettingsDbService : ISettingsDbService
|
|
||||||
{
|
|
||||||
private readonly AssetsDbContext _context;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="SettingsDbService"/> class with the required database context.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">The EF Core context used to access the assets database.</param>
|
|
||||||
public SettingsDbService(AssetsDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Inherits documentation from interface.</summary>
|
|
||||||
public async Task<Settings> GetSettings()
|
|
||||||
{
|
|
||||||
var settings = await _context.Settings.AsNoTracking().FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
if (settings == null)
|
|
||||||
{
|
|
||||||
settings = new Settings { Id = Guid.NewGuid() };
|
|
||||||
_context.Settings.Add(settings);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
_context.ChangeTracker.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Inherits documentation from interface.</summary>
|
|
||||||
public async Task<Settings> SaveSettings(Settings settings)
|
|
||||||
{
|
|
||||||
var existing = await _context.Settings.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
if (existing == null)
|
|
||||||
{
|
|
||||||
if (settings.Id == Guid.Empty)
|
|
||||||
{
|
|
||||||
settings.Id = Guid.NewGuid();
|
|
||||||
}
|
|
||||||
_context.Settings.Add(settings);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
existing.FinishedInitialScan = settings.FinishedInitialScan;
|
|
||||||
existing.TradeRepublicMaxRequestPageSize = settings.TradeRepublicMaxRequestPageSize;
|
|
||||||
existing.AssetUpdateTypeDelay = settings.AssetUpdateTypeDelay;
|
|
||||||
existing.InitAssetUpdateTypeDelay = settings.InitAssetUpdateTypeDelay;
|
|
||||||
existing.InitBatchAssetUpdateDelay = settings.InitBatchAssetUpdateDelay;
|
|
||||||
existing.BatchAssetUpdateDelay = settings.BatchAssetUpdateDelay;
|
|
||||||
existing.CurrentScanningType = settings.CurrentScanningType;
|
|
||||||
existing.CurrentScanningPage = settings.CurrentScanningPage;
|
|
||||||
_context.Settings.Update(existing);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Inherits documentation from interface.</summary>
|
|
||||||
public async Task UpdateSettingsFromDictionary(Dictionary<string, string> dictionary)
|
|
||||||
{
|
|
||||||
var settings = await GetSettings();
|
|
||||||
|
|
||||||
foreach (var (key, value) in dictionary)
|
|
||||||
{
|
|
||||||
if (string.Equals(key, "TradeRepublicMaxRequestPageSize", StringComparison.OrdinalIgnoreCase) && int.TryParse(value, out var ps))
|
|
||||||
settings.TradeRepublicMaxRequestPageSize = ps;
|
|
||||||
else if (string.Equals(key, "AssetUpdateTypeDelay", StringComparison.OrdinalIgnoreCase) && int.TryParse(value, out var autd))
|
|
||||||
settings.AssetUpdateTypeDelay = autd;
|
|
||||||
else if (string.Equals(key, "InitAssetUpdateTypeDelay", StringComparison.OrdinalIgnoreCase) && int.TryParse(value, out var iautd))
|
|
||||||
settings.InitAssetUpdateTypeDelay = iautd;
|
|
||||||
else if (string.Equals(key, "BatchAssetUpdateDelay", StringComparison.OrdinalIgnoreCase) && int.TryParse(value, out var baud))
|
|
||||||
settings.BatchAssetUpdateDelay = baud;
|
|
||||||
else if (string.Equals(key, "InitBatchAssetUpdateDelay", StringComparison.OrdinalIgnoreCase) && int.TryParse(value, out var ibaud))
|
|
||||||
settings.InitBatchAssetUpdateDelay = ibaud;
|
|
||||||
else if (string.Equals(key, "FinishedInitialScan", StringComparison.OrdinalIgnoreCase) && bool.TryParse(value, out var fis))
|
|
||||||
settings.FinishedInitialScan = fis;
|
|
||||||
}
|
|
||||||
|
|
||||||
await SaveSettings(settings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using FinlyticAssets.Entities;
|
using FinlyticAssets.Entities;
|
||||||
using FinlyticCore.Dtos.Assets;
|
using FinlyticCore.Dtos.Assets;
|
||||||
|
|
||||||
@@ -14,6 +17,8 @@ public static class AssetMapper
|
|||||||
Type = t.Type
|
Type = t.Type
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
|
var dynamicLogoUrl = $"/api/v1/logo/{entity.Isin}";
|
||||||
|
|
||||||
return entity switch
|
return entity switch
|
||||||
{
|
{
|
||||||
StockEntity stock => new StockDto
|
StockEntity stock => new StockDto
|
||||||
@@ -23,7 +28,7 @@ public static class AssetMapper
|
|||||||
Type = stock.Type,
|
Type = stock.Type,
|
||||||
InstrumentCategory = stock.InstrumentCategory,
|
InstrumentCategory = stock.InstrumentCategory,
|
||||||
HasCfd = stock.HasCfd,
|
HasCfd = stock.HasCfd,
|
||||||
ImageId = stock.ImageId,
|
ImageId = dynamicLogoUrl,
|
||||||
LastUpdatedAt = stock.LastUpdatedAt,
|
LastUpdatedAt = stock.LastUpdatedAt,
|
||||||
Tags = dtoTags,
|
Tags = dtoTags,
|
||||||
DerivativeProductCategories = stock.DerivativeProductCategories
|
DerivativeProductCategories = stock.DerivativeProductCategories
|
||||||
@@ -35,7 +40,7 @@ public static class AssetMapper
|
|||||||
Type = etf.Type,
|
Type = etf.Type,
|
||||||
InstrumentCategory = etf.InstrumentCategory,
|
InstrumentCategory = etf.InstrumentCategory,
|
||||||
HasCfd = etf.HasCfd,
|
HasCfd = etf.HasCfd,
|
||||||
ImageId = etf.ImageId,
|
ImageId = dynamicLogoUrl,
|
||||||
LastUpdatedAt = etf.LastUpdatedAt,
|
LastUpdatedAt = etf.LastUpdatedAt,
|
||||||
Tags = dtoTags,
|
Tags = dtoTags,
|
||||||
DerivativeProductCategories = etf.DerivativeProductCategories,
|
DerivativeProductCategories = etf.DerivativeProductCategories,
|
||||||
@@ -44,42 +49,44 @@ public static class AssetMapper
|
|||||||
Subtitle = etf.Subtitle,
|
Subtitle = etf.Subtitle,
|
||||||
SearchSubtitle = etf.SearchSubtitle
|
SearchSubtitle = etf.SearchSubtitle
|
||||||
},
|
},
|
||||||
CryptoEntity crypto => new CryptoDto
|
SyntheticEntity synth => new SyntheticDto
|
||||||
{
|
{
|
||||||
Isin = crypto.Isin,
|
Isin = synth.Isin,
|
||||||
Name = crypto.Name,
|
Name = synth.Name,
|
||||||
Type = crypto.Type,
|
Type = synth.Type,
|
||||||
InstrumentCategory = crypto.InstrumentCategory,
|
InstrumentCategory = synth.InstrumentCategory,
|
||||||
HasCfd = crypto.HasCfd,
|
HasCfd = synth.HasCfd,
|
||||||
ImageId = crypto.ImageId,
|
ImageId = dynamicLogoUrl,
|
||||||
LastUpdatedAt = crypto.LastUpdatedAt,
|
LastUpdatedAt = synth.LastUpdatedAt,
|
||||||
Tags = dtoTags,
|
Tags = dtoTags,
|
||||||
Subtitle = crypto.Subtitle,
|
DerivativeProductCategories = synth.DerivativeProductCategories
|
||||||
SearchSubtitle = crypto.SearchSubtitle
|
|
||||||
},
|
},
|
||||||
BondEntity bond => new BondDto
|
_ => new StockDto
|
||||||
{
|
{
|
||||||
Isin = bond.Isin,
|
Isin = entity.Isin,
|
||||||
Name = bond.Name,
|
Name = entity.Name,
|
||||||
Type = bond.Type,
|
Type = entity.Type,
|
||||||
InstrumentCategory = bond.InstrumentCategory,
|
InstrumentCategory = entity.InstrumentCategory,
|
||||||
HasCfd = bond.HasCfd,
|
HasCfd = entity.HasCfd,
|
||||||
ImageId = bond.ImageId,
|
ImageId = dynamicLogoUrl,
|
||||||
LastUpdatedAt = bond.LastUpdatedAt,
|
LastUpdatedAt = entity.LastUpdatedAt,
|
||||||
Tags = dtoTags,
|
Tags = dtoTags
|
||||||
BondIssuerName = bond.BondIssuerName,
|
}
|
||||||
SearchSubtitle = bond.SearchSubtitle
|
};
|
||||||
},
|
}
|
||||||
DerivativeEntity deriv => new DerivativeDto
|
|
||||||
|
public static DerivativeDto ToDto(this DerivativeEntity deriv)
|
||||||
|
{
|
||||||
|
return new DerivativeDto
|
||||||
{
|
{
|
||||||
Isin = deriv.Isin,
|
Isin = deriv.Isin,
|
||||||
Name = deriv.Name,
|
Name = deriv.Name,
|
||||||
Type = deriv.Type,
|
Type = "derivative",
|
||||||
InstrumentCategory = deriv.InstrumentCategory,
|
InstrumentCategory = "derivative",
|
||||||
HasCfd = deriv.HasCfd,
|
HasCfd = false,
|
||||||
ImageId = deriv.ImageId,
|
ImageId = $"/api/v1/logo/{deriv.UnderlyingIsin}",
|
||||||
LastUpdatedAt = deriv.LastUpdatedAt,
|
LastUpdatedAt = deriv.LastUpdatedAt,
|
||||||
Tags = dtoTags,
|
Tags = new List<TagDto>(),
|
||||||
DerivativeProductCategories = deriv.DerivativeProductCategories,
|
DerivativeProductCategories = deriv.DerivativeProductCategories,
|
||||||
UnderlyingIsin = deriv.UnderlyingIsin,
|
UnderlyingIsin = deriv.UnderlyingIsin,
|
||||||
OptionType = deriv.OptionType.ToString(),
|
OptionType = deriv.OptionType.ToString(),
|
||||||
@@ -95,21 +102,7 @@ public static class AssetMapper
|
|||||||
Expiry = deriv.Expiry,
|
Expiry = deriv.Expiry,
|
||||||
Issuer = deriv.Issuer,
|
Issuer = deriv.Issuer,
|
||||||
IssuerDisplayName = deriv.IssuerDisplayName,
|
IssuerDisplayName = deriv.IssuerDisplayName,
|
||||||
IssuerImageId = deriv.IssuerImageId
|
IssuerImageId = null
|
||||||
},
|
|
||||||
SyntheticEntity synth => new SyntheticDto
|
|
||||||
{
|
|
||||||
Isin = synth.Isin,
|
|
||||||
Name = synth.Name,
|
|
||||||
Type = synth.Type,
|
|
||||||
InstrumentCategory = synth.InstrumentCategory,
|
|
||||||
HasCfd = synth.HasCfd,
|
|
||||||
ImageId = synth.ImageId,
|
|
||||||
LastUpdatedAt = synth.LastUpdatedAt,
|
|
||||||
Tags = dtoTags,
|
|
||||||
DerivativeProductCategories = synth.DerivativeProductCategories
|
|
||||||
},
|
|
||||||
_ => throw new NotSupportedException($"Mapping for type {entity.GetType().Name} is not supported.")
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,4 +110,9 @@ public static class AssetMapper
|
|||||||
{
|
{
|
||||||
return entities.Select(e => e.ToDto()).ToList();
|
return entities.Select(e => e.ToDto()).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<DerivativeDto> ToDtoList(this IEnumerable<DerivativeEntity> derivatives)
|
||||||
|
{
|
||||||
|
return derivatives.Select(d => d.ToDto()).ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -6,9 +7,14 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FinlyticAssets.Entities;
|
using FinlyticAssets.Entities;
|
||||||
using FinlyticAssets.Services;
|
using FinlyticAssets.Services;
|
||||||
|
using FinlyticCore.Dtos;
|
||||||
|
using FinlyticCore.Dtos.Assets;
|
||||||
using FinlyticCore.Dtos.Settings;
|
using FinlyticCore.Dtos.Settings;
|
||||||
|
using FinlyticCore.Dtos.TechnicalAnalysis;
|
||||||
using FinlyticCore.Models;
|
using FinlyticCore.Models;
|
||||||
|
using FinlyticCore.Models.Assets;
|
||||||
using FinlyticCore.Services;
|
using FinlyticCore.Services;
|
||||||
|
using FinlyticCore.Services.TradeRepublic;
|
||||||
using FinlyticCore.Util;
|
using FinlyticCore.Util;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -18,13 +24,14 @@ using Microsoft.Extensions.Logging;
|
|||||||
namespace FinlyticAssets.Util;
|
namespace FinlyticAssets.Util;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a managed MQTT client acting as a server-side RPC provider within the asset microservice.
|
/// Managed MQTT client acting as an RPC provider for asset lookups, discovery, and on-demand derivatives.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AssetsMqttClient : ManagedMqttClient, IHostedService
|
public class AssetsMqttClient : ManagedMqttClient, IHostedService
|
||||||
{
|
{
|
||||||
private readonly ILogger<AssetsMqttClient> _logger;
|
private readonly ILogger<AssetsMqttClient> _logger;
|
||||||
private readonly IServiceScopeFactory _scopeFactory;
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly ConcurrentDictionary<string, (LivePriceDto Price, DateTime CachedAt)> _priceCache = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public AssetsMqttClient(
|
public AssetsMqttClient(
|
||||||
ILogger<AssetsMqttClient> logger,
|
ILogger<AssetsMqttClient> logger,
|
||||||
@@ -41,12 +48,7 @@ public class AssetsMqttClient : ManagedMqttClient, IHostedService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task StartAsync(CancellationToken cancellationToken)
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var config = new MqttConfiguration()
|
var config = MqttConfiguration.FromConfiguration(_configuration, "FinlyticAssets");
|
||||||
{
|
|
||||||
Host = _configuration["MQTT:Host"] ?? _configuration["MQTT__Host"] ?? "localhost",
|
|
||||||
Port = Convert.ToInt32(_configuration["MQTT:Port"] ?? _configuration["MQTT__Port"] ?? "1883"),
|
|
||||||
ClientId = $"{(_configuration["MQTT:ClientId"] ?? _configuration["MQTT__ClientId"] ?? "FinlyticAssets")}_{Guid.NewGuid()}"
|
|
||||||
};
|
|
||||||
|
|
||||||
_logger.LogInformation("Starting Assets MQTT client. Host: {Host}, ClientId: {ClientId}", config.Host, config.ClientId);
|
_logger.LogInformation("Starting Assets MQTT client. Host: {Host}, ClientId: {ClientId}", config.Host, config.ClientId);
|
||||||
await ConnectAsync(config);
|
await ConnectAsync(config);
|
||||||
@@ -66,278 +68,158 @@ public class AssetsMqttClient : ManagedMqttClient, IHostedService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected override async Task OnConnectedAsync()
|
protected override async Task OnConnectedAsync()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Assets MQTT Client connected. Subscribing to topics...");
|
_logger.LogInformation("Assets MQTT Client connected. Registering topic subscriptions...");
|
||||||
await SubscribeAsync("services/request/assets_Get/#");
|
|
||||||
await SubscribeAsync("services/request/assets_Search/#");
|
|
||||||
await SubscribeAsync("services/request/assets_GetDiscovery/#");
|
|
||||||
await SubscribeAsync("services/request/assets_GetDerivatives/#");
|
|
||||||
await SubscribeAsync("services/request/assets_FetchLogo/#");
|
|
||||||
await SubscribeAsync("services/request/assets_settings_GetAll/#");
|
|
||||||
await SubscribeAsync("services/request/assets_settings_Update/#");
|
|
||||||
await SubscribeAsync("services/request/health_Ping/#");
|
|
||||||
await SubscribeAsync("services/config/updated/#");
|
|
||||||
|
|
||||||
FinlyticCore.Services.FinlyticLogBroadcaster.OnLogPublished = async (logDto) =>
|
await SubscribeAsync(MqttTopics.ResponseWildcard);
|
||||||
|
await SubscribeRpcAsync<GetValidAssetRequest, List<AssetDto>>(MqttTopics.RequestFilter(MqttTopics.Channels.AssetsGet), HandleAssetsGetRpcAsync);
|
||||||
|
await SubscribeRpcAsync<GetDiscoveryAssetsRequest, List<AssetDto>>(MqttTopics.RequestFilter(MqttTopics.Channels.AssetsGetDiscovery), HandleAssetsGetDiscoveryRpcAsync);
|
||||||
|
await SubscribeRpcAsync<GetDerivativesRequest, List<DerivativeDto>>(MqttTopics.RequestFilter(MqttTopics.Channels.AssetsGetDerivatives), HandleAssetsGetDerivativesRpcAsync);
|
||||||
|
await SubscribeRpcAsync<IsinRequest, LivePriceDto?>(MqttTopics.RequestFilter(MqttTopics.Channels.TrGetLivePrice), HandleGetLivePriceRpcAsync);
|
||||||
|
await SubscribeRpcAsync<object, List<DynamicSettingDto>>(MqttTopics.RequestFilter(MqttTopics.Channels.AssetsSettingsGetAll), HandleSettingsGetAllRpcAsync);
|
||||||
|
await SubscribeRpcAsync<Dictionary<string, object?>, List<DynamicSettingDto>>(MqttTopics.RequestFilter(MqttTopics.Channels.AssetsSettingsUpdate), HandleSettingsUpdateRpcAsync);
|
||||||
|
await SubscribeAsync<object>(MqttTopics.RequestFilter(MqttTopics.Channels.HealthPing), HandleHealthPingRpcAsync);
|
||||||
|
|
||||||
|
FinlyticLogBroadcaster.OnLogPublished = async (logDto) =>
|
||||||
{
|
{
|
||||||
if (IsConnected && string.Equals(logDto.ServiceName, "FinlyticAssets", StringComparison.OrdinalIgnoreCase))
|
if (IsConnected && string.Equals(logDto.ServiceName, "FinlyticAssets", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
await PublishAsync("finlytic/logs/FinlyticAssets", logDto);
|
await PublishAsync(MqttTopics.Logs("FinlyticAssets"), logDto);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private async Task<List<AssetDto>> HandleAssetsGetRpcAsync(GetValidAssetRequest? req, string correlationId)
|
||||||
/// Processes incoming messages on the subscribed topics.
|
|
||||||
/// </summary>
|
|
||||||
protected override async Task OnMessageReceivedAsync(string topic, string payload)
|
|
||||||
{
|
{
|
||||||
if (topic.StartsWith("services/config/updated", StringComparison.OrdinalIgnoreCase))
|
if (req == null || string.IsNullOrWhiteSpace(req.Isin)) return [];
|
||||||
{
|
|
||||||
await HandleConfigUpdatedAsync(topic, payload);
|
using var scope = _scopeFactory.CreateScope();
|
||||||
return;
|
var dbService = scope.ServiceProvider.GetRequiredService<IAssetsDbService>();
|
||||||
|
var assets = await dbService.GetValidAssetsByIsinAsync(req.Isin.Trim().ToUpperInvariant());
|
||||||
|
return assets.ToDtoList();
|
||||||
}
|
}
|
||||||
|
|
||||||
var segments = topic.Split('/');
|
private async Task<List<AssetDto>> HandleAssetsGetDiscoveryRpcAsync(GetDiscoveryAssetsRequest? req, string correlationId)
|
||||||
if (segments.Length < 4) return;
|
|
||||||
|
|
||||||
var channel = segments[2];
|
|
||||||
var correlationId = segments[segments.Length - 1];
|
|
||||||
|
|
||||||
if (topic.Contains("health_Ping", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
await HandleHealthPingAsync(topic, segments, correlationId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (topic.StartsWith("services/request/assets_settings_GetAll", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
await HandleSettingsGetAllAsync(correlationId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (topic.StartsWith("services/request/assets_settings_Update", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
await HandleSettingsUpdateAsync(payload, correlationId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
using var scope = _scopeFactory.CreateScope();
|
using var scope = _scopeFactory.CreateScope();
|
||||||
var dbService = scope.ServiceProvider.GetRequiredService<IAssetsDbService>();
|
var dbService = scope.ServiceProvider.GetRequiredService<IAssetsDbService>();
|
||||||
var indexService = scope.ServiceProvider.GetRequiredService<IAssetsIndexService>();
|
int limit = req?.Limit > 0 ? req.Limit : 15;
|
||||||
|
var discoveryAssets = await dbService.GetDiscoveryAssetsAsync(limit);
|
||||||
|
return discoveryAssets.ToDtoList();
|
||||||
|
}
|
||||||
|
|
||||||
if (channel == "assets_FetchLogo")
|
private async Task<List<DerivativeDto>> HandleAssetsGetDerivativesRpcAsync(GetDerivativesRequest? req, string correlationId)
|
||||||
{
|
{
|
||||||
await HandleFetchLogoAsync(payload, correlationId, indexService);
|
if (req == null || string.IsNullOrWhiteSpace(req.UnderlyingIsin)) return [];
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<AssetEntity> responseData = [];
|
|
||||||
|
|
||||||
switch (channel)
|
|
||||||
{
|
|
||||||
case "assets_Get":
|
|
||||||
responseData = await HandleAssetsGetAsync(payload, dbService);
|
|
||||||
break;
|
|
||||||
case "assets_Search":
|
|
||||||
responseData = await HandleAssetsSearchAsync(payload, dbService);
|
|
||||||
break;
|
|
||||||
case "assets_GetDiscovery":
|
|
||||||
responseData = await HandleAssetsGetDiscoveryAsync(payload, dbService);
|
|
||||||
break;
|
|
||||||
case "assets_GetDerivatives":
|
|
||||||
responseData = (await HandleAssetsGetDerivativesAsync(payload, dbService)).Cast<AssetEntity>().ToList();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
string defaultResponseTopic = $"services/response/{channel}/{correlationId}";
|
|
||||||
await PublishAsync(defaultResponseTopic, responseData.ToDtoList());
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
OnError(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleSettingsGetAllAsync(string correlationId)
|
|
||||||
{
|
|
||||||
using var scope = _scopeFactory.CreateScope();
|
|
||||||
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<AssetsMqttClient>>();
|
|
||||||
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
|
||||||
|
|
||||||
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticAssets] [Settings_GetAll] Retrieving all dynamic settings via reflection [CorrelationId: {CorrelationId}]", correlationId);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var settings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) });
|
|
||||||
var responseTopic = $"services/response/assets_settings_GetAll/{correlationId}";
|
|
||||||
|
|
||||||
await PublishAsync(responseTopic, settings);
|
|
||||||
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticAssets] [Settings_GetAll] Published {Count} settings to '{ResponseTopic}'", settings.Count, responseTopic);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticAssets] [Settings_GetAll] Failed to retrieve settings.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleSettingsUpdateAsync(string payload, string correlationId)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(payload)) return;
|
|
||||||
|
|
||||||
using var scope = _scopeFactory.CreateScope();
|
using var scope = _scopeFactory.CreateScope();
|
||||||
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<AssetsMqttClient>>();
|
var dbService = scope.ServiceProvider.GetRequiredService<IAssetsDbService>();
|
||||||
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
var derivatives = await dbService.GetDerivativesByUnderlyingAsync(
|
||||||
|
req.UnderlyingIsin.Trim().ToUpperInvariant(),
|
||||||
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticAssets] [Settings_Update] Processing settings update RPC [CorrelationId: {CorrelationId}]", correlationId);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Dictionary<string, object?>? updates = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
updates = JsonSerializer.Deserialize<Dictionary<string, object?>>(payload);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
var list = JsonSerializer.Deserialize<List<DynamicSettingDto>>(payload);
|
|
||||||
if (list != null)
|
|
||||||
{
|
|
||||||
updates = new Dictionary<string, object?>();
|
|
||||||
foreach (var item in list) updates[item.Key] = item.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updates != null && updates.Count > 0)
|
|
||||||
{
|
|
||||||
await settingsService.UpdateSettingsAsync(updates);
|
|
||||||
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticAssets] [Settings_Update] Successfully updated {Count} settings in database and cache.", updates.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentSettings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) });
|
|
||||||
var responseTopic = $"services/response/assets_settings_Update/{correlationId}";
|
|
||||||
await PublishAsync(responseTopic, currentSettings);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticAssets] [Settings_Update] Failed to update settings.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleConfigUpdatedAsync(string topic, string payload)
|
|
||||||
{
|
|
||||||
if (!topic.EndsWith("FinlyticAssets", StringComparison.OrdinalIgnoreCase))
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var updatePayload = JsonSerializer.Deserialize(payload, FinlyticJsonSerializerContext.Default.ServiceConfigUpdatePayload);
|
|
||||||
if (updatePayload?.Settings != null && updatePayload.Settings.Count > 0)
|
|
||||||
{
|
|
||||||
using var scope = _scopeFactory.CreateScope();
|
|
||||||
var settings = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
|
||||||
var dict = updatePayload.Settings.ToDictionary(k => k.Key, v => (object?)v.Value);
|
|
||||||
await settings.UpdateSettingsAsync(dict);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
using var scope = _scopeFactory.CreateScope();
|
|
||||||
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<AssetsMqttClient>>();
|
|
||||||
await finlyticLogger.LogErrorAsync(SettingKeys.AssetsChannel, ex, "[AssetsMqttClient] Error processing MQTT config update event.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleHealthPingAsync(string topic, string[] segments, string correlationId)
|
|
||||||
{
|
|
||||||
bool isForMe = segments.Length >= 5
|
|
||||||
? segments[3].Equals("FinlyticAssets", StringComparison.OrdinalIgnoreCase)
|
|
||||||
: topic.Contains("FinlyticAssets", StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
if (isForMe)
|
|
||||||
{
|
|
||||||
string respTopic = $"services/response/health_Ping/{correlationId}";
|
|
||||||
await PublishAsync(respTopic, new FinlyticCore.Dtos.ServiceHealthResponse("FinlyticAssets", "Online", DateTime.UtcNow, "Connected"));
|
|
||||||
using var scope = _scopeFactory.CreateScope();
|
|
||||||
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<AssetsMqttClient>>();
|
|
||||||
await finlyticLogger.LogInfoAsync(SettingKeys.HealthPingChannel, "[AssetsMqttClient] Responded to live health_Ping RPC request [CorrelationId: {CorrelationId}].", correlationId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleFetchLogoAsync(string payload, string correlationId, IAssetsIndexService indexService)
|
|
||||||
{
|
|
||||||
var req = JsonSerializer.Deserialize(payload, FinlyticJsonSerializerContext.Default.IsinRequest);
|
|
||||||
string? isin = req?.Isin;
|
|
||||||
string? savedPath = null;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(isin))
|
|
||||||
{
|
|
||||||
savedPath = await indexService.DownloadAndSaveLogoAsync(isin);
|
|
||||||
}
|
|
||||||
|
|
||||||
string responseTopic = $"services/response/assets_FetchLogo/{correlationId}";
|
|
||||||
await PublishAsync(responseTopic, new FinlyticCore.Dtos.FetchLogoResponse(isin, savedPath, savedPath != null));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<AssetEntity>> HandleAssetsGetAsync(string payload, IAssetsDbService dbService)
|
|
||||||
{
|
|
||||||
var validReq = JsonSerializer.Deserialize(payload, FinlyticJsonSerializerContext.Default.GetValidAssetRequest);
|
|
||||||
if (validReq != null)
|
|
||||||
{
|
|
||||||
return await dbService.GetValidAssetsByIsinAsync(validReq.Isin);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<AssetEntity>> HandleAssetsSearchAsync(string payload, IAssetsDbService dbService)
|
|
||||||
{
|
|
||||||
var searchReq = JsonSerializer.Deserialize(payload, FinlyticJsonSerializerContext.Default.SearchAssetsRequest);
|
|
||||||
if (searchReq != null)
|
|
||||||
{
|
|
||||||
return await dbService.FindAffectedActiveAssetsAsync(searchReq.SearchQuery);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<AssetEntity>> HandleAssetsGetDiscoveryAsync(string payload, IAssetsDbService dbService)
|
|
||||||
{
|
|
||||||
int limit = 15;
|
|
||||||
if (!string.IsNullOrWhiteSpace(payload))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var discReq = JsonSerializer.Deserialize(payload, FinlyticJsonSerializerContext.Default.GetDiscoveryAssetsRequest);
|
|
||||||
if (discReq != null && discReq.Limit > 0) limit = discReq.Limit;
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
return await dbService.GetDiscoveryAssetsAsync(limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<DerivativeEntity>> HandleAssetsGetDerivativesAsync(string payload, IAssetsDbService dbService)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(payload)) return [];
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var req = JsonSerializer.Deserialize(payload, FinlyticJsonSerializerContext.Default.GetDerivativesRequest);
|
|
||||||
if (req != null && !string.IsNullOrEmpty(req.UnderlyingIsin))
|
|
||||||
{
|
|
||||||
return await dbService.GetDerivativesByUnderlyingAsync(
|
|
||||||
req.UnderlyingIsin,
|
|
||||||
req.OptionType,
|
req.OptionType,
|
||||||
req.TargetLeverage,
|
req.TargetLeverage,
|
||||||
req.After,
|
req.After,
|
||||||
req.Page,
|
req.Page,
|
||||||
req.ShouldForceRefresh);
|
req.ShouldForceRefresh);
|
||||||
|
return derivatives.ToDtoList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<LivePriceDto?> HandleGetLivePriceRpcAsync(IsinRequest? req, string correlationId)
|
||||||
|
{
|
||||||
|
if (req == null || string.IsNullOrWhiteSpace(req.Isin)) return null;
|
||||||
|
|
||||||
|
var cleanIsin = req.Isin.Trim().ToUpperInvariant();
|
||||||
|
|
||||||
|
// Return fresh price from cache if less than 3 seconds old
|
||||||
|
if (_priceCache.TryGetValue(cleanIsin, out var cached) && (DateTime.UtcNow - cached.CachedAt).TotalSeconds < 3)
|
||||||
|
{
|
||||||
|
return cached.Price;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var trService = scope.ServiceProvider.GetRequiredService<ITradeRepublicService>();
|
||||||
|
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<AssetsMqttClient>>();
|
||||||
|
|
||||||
|
var tcs = new TaskCompletionSource<LivePriceDto?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
int? subId = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
subId = await trService.SubscribeRealtimeTickerAsync(cleanIsin, tick =>
|
||||||
|
{
|
||||||
|
decimal currentPrice = tick.Last?.PriceValue > 0 ? tick.Last.PriceValue :
|
||||||
|
(tick.Bid?.PriceValue > 0 && tick.Ask?.PriceValue > 0 ? (tick.Bid.PriceValue + tick.Ask.PriceValue) / 2m :
|
||||||
|
(tick.Ask?.PriceValue ?? tick.Bid?.PriceValue ?? 0m));
|
||||||
|
|
||||||
|
decimal preClose = tick.Pre?.PriceValue > 0 ? tick.Pre.PriceValue : (tick.Open?.PriceValue ?? 0m);
|
||||||
|
decimal dailyChange = preClose > 0m ? ((currentPrice - preClose) / preClose) * 100m : 0m;
|
||||||
|
|
||||||
|
var livePrice = new LivePriceDto(
|
||||||
|
Isin: cleanIsin,
|
||||||
|
CurrentPrice: currentPrice,
|
||||||
|
DailyChangePercent: dailyChange,
|
||||||
|
Bid: tick.Bid?.PriceValue,
|
||||||
|
Ask: tick.Ask?.PriceValue
|
||||||
|
);
|
||||||
|
|
||||||
|
_priceCache[cleanIsin] = (livePrice, DateTime.UtcNow);
|
||||||
|
tcs.TrySetResult(livePrice);
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(3));
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
using var scope = _scopeFactory.CreateScope();
|
await finlyticLogger.LogWarningAsync(SettingKeys.AssetsChannel, "[AssetsMqttClient] Failed or timed out fetching live price for {Isin}: {Message}", cleanIsin, ex.Message);
|
||||||
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<AssetsMqttClient>>();
|
if (_priceCache.TryGetValue(cleanIsin, out var stale))
|
||||||
await finlyticLogger.LogErrorAsync(SettingKeys.AssetsChannel, ex, "[AssetsMqttClient] Error parsing GetDerivativesRequest payload.");
|
{
|
||||||
|
return stale.Price;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (subId.HasValue)
|
||||||
|
{
|
||||||
|
_ = trService.UnsubscribeRealtimeTickerAsync(subId.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
private async Task<List<DynamicSettingDto>> HandleSettingsGetAllRpcAsync(object? _, string correlationId)
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<AssetsMqttClient>>();
|
||||||
|
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
||||||
|
|
||||||
|
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticAssets] [Settings_GetAll] Retrieving service dynamic settings [CorrelationId: {CorrelationId}]", correlationId);
|
||||||
|
return await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<DynamicSettingDto>> HandleSettingsUpdateRpcAsync(Dictionary<string, object?>? updates, string correlationId)
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<AssetsMqttClient>>();
|
||||||
|
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
||||||
|
|
||||||
|
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticAssets] [Settings_Update] Processing settings update RPC [CorrelationId: {CorrelationId}]", correlationId);
|
||||||
|
if (updates != null && updates.Count > 0)
|
||||||
|
{
|
||||||
|
await settingsService.UpdateSettingsAsync(updates);
|
||||||
|
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticAssets] [Settings_Update] Successfully updated {Count} settings in database and cache.", updates.Count);
|
||||||
|
}
|
||||||
|
return await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleHealthPingRpcAsync(object? _, string topic, string correlationId)
|
||||||
|
{
|
||||||
|
if (topic.Contains("FinlyticAssets", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
string respTopic = MqttTopics.ResponseTopic(MqttTopics.Channels.HealthPing, correlationId);
|
||||||
|
await PublishAsync(respTopic, new FinlyticCore.Dtos.ServiceHealthResponse("FinlyticAssets", "Online", DateTime.UtcNow, "Connected"));
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<AssetsMqttClient>>();
|
||||||
|
await finlyticLogger.LogInfoAsync(SettingKeys.HealthPingChannel, "[AssetsMqttClient] Responded to health_Ping RPC request [CorrelationId: {CorrelationId}].", correlationId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,21 @@ public static class SettingKeys
|
|||||||
public static readonly SettingKey<bool> AssetsChannel = new("Logging.Channel.Assets", true);
|
public static readonly SettingKey<bool> AssetsChannel = new("Logging.Channel.Assets", true);
|
||||||
public static readonly SettingKey<bool> MqttChannel = new("Logging.Channel.MQTT", true);
|
public static readonly SettingKey<bool> MqttChannel = new("Logging.Channel.MQTT", true);
|
||||||
public static readonly SettingKey<bool> HealthPingChannel = new("Logging.Channel.Health", true);
|
public static readonly SettingKey<bool> HealthPingChannel = new("Logging.Channel.Health", true);
|
||||||
|
public static readonly SettingKey<bool> TradeRepublicChannel = new("Logging.Channel.TradeRepublic", true);
|
||||||
|
|
||||||
// --- Asset Scanning ---
|
// --- Trade Republic Connection ---
|
||||||
public static readonly SettingKey<bool> EnableAutoScan = new("Scanner.EnableAutoScan", true);
|
public static readonly SettingKey<int> TradeRepublicWsReconnectInterval = new("TradeRepublic.WsReconnectIntervalSeconds", 5);
|
||||||
public static readonly SettingKey<int> ScanIntervalHours = new("Scanner.ScanIntervalHours", 12);
|
public static readonly SettingKey<int> TradeRepublicWsTimeout = new("TradeRepublic.WsTimeoutSeconds", 15);
|
||||||
public static readonly SettingKey<int> MaxConcurrentScans = new("Scanner.MaxConcurrentScans", 5);
|
|
||||||
public static readonly SettingKey<bool> EnableDerivativeScanning = new("Scanner.EnableDerivativeScanning", true);
|
|
||||||
|
|
||||||
// --- Logos & Media ---
|
// --- Asset Scanning Config ---
|
||||||
public static readonly SettingKey<bool> AutoFetchLogos = new("Media.AutoFetchLogos", true);
|
public static readonly SettingKey<bool> ScannerEnableAutoScan = new("Scanner.EnableAutoScan", true);
|
||||||
public static readonly SettingKey<int> LogoFetchBatchSize = new("Media.LogoFetchBatchSize", 25);
|
public static readonly SettingKey<string> ScannerCurrentScanningType = new("Scanner.CurrentScanningType", "Stock");
|
||||||
public static readonly SettingKey<string> LogoStorageDirectory = new("Media.LogoStorageDirectory", "data/logos");
|
public static readonly SettingKey<int> ScannerCurrentScanningPage = new("Scanner.CurrentScanningPage", 0);
|
||||||
|
public static readonly SettingKey<bool> ScannerFinishedInitialScan = new("Scanner.FinishedInitialScan", false);
|
||||||
|
public static readonly SettingKey<int> ScannerBatchDelay = new("Scanner.BatchAssetUpdateDelay", 0);
|
||||||
|
public static readonly SettingKey<int> ScannerTypeDelay = new("Scanner.AssetUpdateTypeDelay", 0);
|
||||||
|
public static readonly SettingKey<int> ScannerInitBatchDelay = new("Scanner.InitBatchAssetUpdateDelay", 0);
|
||||||
|
public static readonly SettingKey<int> ScannerInitTypeDelay = new("Scanner.InitAssetUpdateTypeDelay", 0);
|
||||||
|
public static readonly SettingKey<int> ScannerMaxPageSize = new("Scanner.TradeRepublicMaxRequestPageSize", 50);
|
||||||
|
public static readonly SettingKey<int> ScannerCycleDelayMinutes = new("Scanner.CycleDelayMinutes", 1440);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
namespace FinlyticAssets.Util;
|
|
||||||
|
|
||||||
public class StringCodeGenerator
|
|
||||||
{
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generates a W3C traceparent string for telemetry tracking.
|
|
||||||
/// </summary>
|
|
||||||
public static string GenerateTraceparent()
|
|
||||||
{
|
|
||||||
var traceId = Guid.NewGuid().ToString("N");
|
|
||||||
var spanId = Guid.NewGuid().ToString("N").Substring(0, 16);
|
|
||||||
|
|
||||||
return $"00-{traceId}-{spanId}-01";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user