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<Settings> Settings { get; set; }
|
||||
public DbSet<AssetEntity> TradeRepublicAssets { get; set; }
|
||||
public DbSet<TagEntity> TradeRepublicTags { get; set; }
|
||||
public DbSet<AssetEntity> TradeRepublicAssets => Set<AssetEntity>();
|
||||
public DbSet<DerivativeEntity> Derivatives => Set<DerivativeEntity>();
|
||||
public DbSet<TagEntity> TradeRepublicTags => Set<TagEntity>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -30,20 +30,29 @@ public class AssetsDbContext : DbContext, ISettingsDbContext
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.HasIndex(e => e.Key).IsUnique();
|
||||
});
|
||||
|
||||
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.HasDiscriminator<string>("AssetType")
|
||||
.HasValue<StockEntity>("Stock")
|
||||
.HasValue<CryptoEntity>("Crypto")
|
||||
.HasValue<EtfEntity>("Etf")
|
||||
.HasValue<SyntheticEntity>("Synthetic")
|
||||
.HasValue<BondEntity>("Bond")
|
||||
.HasValue<DerivativeEntity>("Derivative");
|
||||
.HasValue<SyntheticEntity>("Synthetic");
|
||||
});
|
||||
|
||||
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 =
|
||||
new Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<List<string>, string>(
|
||||
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<Settings>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
entity.Property(e => e.CurrentScanningType)
|
||||
.HasConversion<string>()
|
||||
.HasMaxLength(50);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<AssetEntity>()
|
||||
.HasMany(a => a.Tags)
|
||||
.WithMany(t => t.Assets);
|
||||
|
||||
@@ -5,6 +5,7 @@ WORKDIR /app
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["FinlyticCore/FinlyticCore.csproj", "FinlyticCore/"]
|
||||
COPY ["FinlyticAssets/FinlyticAssets.csproj", "FinlyticAssets/"]
|
||||
RUN dotnet restore "FinlyticAssets/FinlyticAssets.csproj"
|
||||
COPY . .
|
||||
|
||||
@@ -7,8 +7,6 @@ public abstract class AssetEntity
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string InstrumentCategory { get; set; } = string.Empty;
|
||||
public bool HasCfd { get; set; }
|
||||
public string? ImageId { get; set; }
|
||||
|
||||
public DateTime LastUpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public List<TagEntity> Tags { get; set; } = new();
|
||||
|
||||
@@ -11,11 +11,17 @@ public enum OptionType
|
||||
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) ---
|
||||
[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 ---
|
||||
public OptionType OptionType { get; set; } // Long / Short
|
||||
@@ -54,7 +60,8 @@ public class DerivativeEntity : AssetEntity
|
||||
[MaxLength(150)]
|
||||
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
|
||||
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
|
||||
{
|
||||
[DbContext(typeof(AssetsDbContext))]
|
||||
[Migration("20260815184053_UpdateDynamicSettingsUniqueIndex")]
|
||||
partial class UpdateDynamicSettingsUniqueIndex
|
||||
[Migration("20260818185753_Init")]
|
||||
partial class Init
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@@ -59,9 +59,6 @@ namespace FinlyticAssets.Migrations
|
||||
b.Property<bool>("HasCfd")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("ImageId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("LastUpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
@@ -84,41 +81,91 @@ namespace FinlyticAssets.Migrations
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticAssets.Entities.Settings", b =>
|
||||
modelBuilder.Entity("FinlyticAssets.Entities.DerivativeEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
b.Property<string>("Isin")
|
||||
.HasMaxLength(12)
|
||||
.HasColumnType("character varying(12)");
|
||||
|
||||
b.Property<int>("AssetUpdateTypeDelay")
|
||||
.HasColumnType("integer");
|
||||
b.Property<decimal>("Barrier")
|
||||
.HasColumnType("numeric(18,6)");
|
||||
|
||||
b.Property<int>("BatchAssetUpdateDelay")
|
||||
.HasColumnType("integer");
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("CurrentScanningPage")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("CurrentScanningType")
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<bool>("FinishedInitialScan")
|
||||
.HasColumnType("boolean");
|
||||
b.Property<decimal?>("Delta")
|
||||
.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");
|
||||
|
||||
b.Property<int>("InitBatchAssetUpdateDelay")
|
||||
.HasColumnType("integer");
|
||||
b.Property<string>("ProductCategoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("TradeRepublicMaxRequestPageSize")
|
||||
.HasColumnType("integer");
|
||||
b.Property<decimal?>("Size")
|
||||
.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 =>
|
||||
@@ -170,109 +217,6 @@ namespace FinlyticAssets.Migrations
|
||||
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");
|
||||
@@ -297,18 +241,6 @@ namespace FinlyticAssets.Migrations
|
||||
.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");
|
||||
});
|
||||
|
||||
+69
-22
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
@@ -12,22 +12,47 @@ namespace FinlyticAssets.Migrations
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Settings",
|
||||
name: "Derivatives",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FinishedInitialScan = table.Column<bool>(type: "boolean", nullable: false),
|
||||
TradeRepublicMaxRequestPageSize = table.Column<int>(type: "integer", nullable: false),
|
||||
AssetUpdateTypeDelay = table.Column<int>(type: "integer", nullable: false),
|
||||
InitAssetUpdateTypeDelay = table.Column<int>(type: "integer", nullable: false),
|
||||
InitBatchAssetUpdateDelay = table.Column<int>(type: "integer", nullable: false),
|
||||
BatchAssetUpdateDelay = table.Column<int>(type: "integer", nullable: false),
|
||||
CurrentScanningType = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
CurrentScanningPage = table.Column<int>(type: "integer", nullable: false)
|
||||
Isin = table.Column<string>(type: "character varying(12)", maxLength: 12, nullable: false),
|
||||
UnderlyingIsin = table.Column<string>(type: "character varying(12)", maxLength: 12, nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
OptionType = table.Column<int>(type: "integer", nullable: false),
|
||||
ProductCategoryName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
NextGenProductCategoryName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Strike = table.Column<decimal>(type: "numeric(18,6)", nullable: false),
|
||||
Barrier = table.Column<decimal>(type: "numeric(18,6)", 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 =>
|
||||
{
|
||||
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(
|
||||
@@ -39,20 +64,13 @@ namespace FinlyticAssets.Migrations
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
Type = table.Column<string>(type: "text", 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),
|
||||
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),
|
||||
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),
|
||||
MappedEtfIndexName = table.Column<string>(type: "text", nullable: true),
|
||||
EtfEntity_Subtitle = table.Column<string>(type: "text", nullable: true),
|
||||
EtfEntity_SearchSubtitle = table.Column<string>(type: "text", nullable: true),
|
||||
Subtitle = table.Column<string>(type: "text", nullable: true),
|
||||
SearchSubtitle = 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)
|
||||
},
|
||||
@@ -104,6 +122,32 @@ namespace FinlyticAssets.Migrations
|
||||
table: "AssetEntityTagEntity",
|
||||
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(
|
||||
name: "IX_TradeRepublicAssets_LastUpdatedAt",
|
||||
table: "TradeRepublicAssets",
|
||||
@@ -117,7 +161,10 @@ namespace FinlyticAssets.Migrations
|
||||
name: "AssetEntityTagEntity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Settings");
|
||||
name: "Derivatives");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DynamicSettings");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TradeRepublicAssets");
|
||||
+77
-112
@@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
namespace FinlyticAssets.Migrations
|
||||
{
|
||||
[DbContext(typeof(AssetsDbContext))]
|
||||
[Migration("20260813202348_UpdateDerivativeAssets")]
|
||||
partial class UpdateDerivativeAssets
|
||||
[Migration("20260821153552_SyncAssetModelDrift")]
|
||||
partial class SyncAssetModelDrift
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@@ -59,9 +59,6 @@ namespace FinlyticAssets.Migrations
|
||||
b.Property<bool>("HasCfd")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("ImageId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("LastUpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
@@ -84,104 +81,18 @@ namespace FinlyticAssets.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.HasBaseType("FinlyticAssets.Entities.AssetEntity");
|
||||
b.Property<string>("Isin")
|
||||
.HasMaxLength(12)
|
||||
.HasColumnType("character varying(12)");
|
||||
|
||||
b.Property<decimal>("Barrier")
|
||||
.HasColumnType("numeric(18,6)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
@@ -210,12 +121,16 @@ namespace FinlyticAssets.Migrations
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("character varying(150)");
|
||||
|
||||
b.Property<string>("IssuerImageId")
|
||||
.HasColumnType("text");
|
||||
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)
|
||||
@@ -236,10 +151,72 @@ namespace FinlyticAssets.Migrations
|
||||
.HasColumnType("numeric(18,6)");
|
||||
|
||||
b.Property<string>("UnderlyingIsin")
|
||||
.IsRequired()
|
||||
.HasMaxLength(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 =>
|
||||
@@ -266,18 +243,6 @@ namespace FinlyticAssets.Migrations
|
||||
.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");
|
||||
});
|
||||
|
||||
@@ -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")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("ImageId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("LastUpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
@@ -81,41 +78,93 @@ namespace FinlyticAssets.Migrations
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticAssets.Entities.Settings", b =>
|
||||
modelBuilder.Entity("FinlyticAssets.Entities.DerivativeEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
b.Property<string>("Isin")
|
||||
.HasMaxLength(12)
|
||||
.HasColumnType("character varying(12)");
|
||||
|
||||
b.Property<int>("AssetUpdateTypeDelay")
|
||||
.HasColumnType("integer");
|
||||
b.Property<decimal>("Barrier")
|
||||
.HasColumnType("numeric(18,6)");
|
||||
|
||||
b.Property<int>("BatchAssetUpdateDelay")
|
||||
.HasColumnType("integer");
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("CurrentScanningPage")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("CurrentScanningType")
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<bool>("FinishedInitialScan")
|
||||
.HasColumnType("boolean");
|
||||
b.Property<decimal?>("Delta")
|
||||
.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");
|
||||
|
||||
b.Property<int>("InitBatchAssetUpdateDelay")
|
||||
.HasColumnType("integer");
|
||||
b.Property<string>("ProductCategoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("TradeRepublicMaxRequestPageSize")
|
||||
.HasColumnType("integer");
|
||||
b.Property<decimal?>("Size")
|
||||
.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 =>
|
||||
@@ -167,109 +216,6 @@ namespace FinlyticAssets.Migrations
|
||||
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");
|
||||
@@ -294,18 +240,6 @@ namespace FinlyticAssets.Migrations
|
||||
.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");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using FinlyticAssets.Database;
|
||||
using FinlyticAssets.Services;
|
||||
using FinlyticAssets.Util;
|
||||
@@ -24,14 +23,12 @@ builder.Services.AddSingleton(typeof(IFinlyticLogger<>), typeof(FinlyticLogger<>
|
||||
builder.Services.AddScoped<TradeRepublicClient>();
|
||||
builder.Services.AddScoped<ITradeRepublicService, TradeRepublicService>();
|
||||
builder.Services.AddScoped<IAssetsDbService, AssetsDbService>();
|
||||
builder.Services.AddScoped<ISettingsDbService, SettingsDbService>();
|
||||
builder.Services.AddScoped<IAssetsIndexService, AssetsIndexService>();
|
||||
|
||||
builder.Services.AddSingleton<AssetsMqttClient>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<AssetsMqttClient>());
|
||||
|
||||
builder.Services.AddHostedService<AssetScannerBackgroundService>();
|
||||
builder.Services.AddHostedService<LogoFetcherBackgroundService>();
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
@@ -40,21 +37,14 @@ using (var scope = host.Services.CreateScope())
|
||||
try
|
||||
{
|
||||
var context = scope.ServiceProvider.GetRequiredService<AssetsDbContext>();
|
||||
await context.Database.MigrateAsync();
|
||||
|
||||
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.");
|
||||
}
|
||||
var connStr = builder.Configuration.GetConnectionString("DefaultConnection") ?? "";
|
||||
await context.MigrateWithBootstrapAsync(connStr);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Critical error during database migration for FinlyticAssets: {ex.Message}");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticAssets.Models;
|
||||
using FinlyticAssets.Util;
|
||||
using FinlyticCore.Dtos.TradeRepublic;
|
||||
using FinlyticCore.Models.Assets;
|
||||
@@ -15,14 +15,16 @@ using Microsoft.Extensions.Hosting;
|
||||
namespace FinlyticAssets.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A background service that runs a continuous asset synchronization loop,
|
||||
/// scanning Trade Republic to retrieve, update, and index all supported asset types.
|
||||
/// A background service that runs a continuous asset synchronization loop for Stocks and ETFs,
|
||||
/// retrieving, updating, and indexing assets while downloading logos inline.
|
||||
/// </summary>
|
||||
public class AssetScannerBackgroundService : BackgroundService
|
||||
{
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly IFinlyticLogger<AssetScannerBackgroundService> _finlyticLogger;
|
||||
|
||||
private static readonly AssetType[] ScannedAssetTypes = [AssetType.Stock, AssetType.Fund];
|
||||
|
||||
private AssetsCount? _assetsCount;
|
||||
private AssetsCount? _currAssetsCount;
|
||||
|
||||
@@ -54,24 +56,35 @@ public class AssetScannerBackgroundService : BackgroundService
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
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 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...");
|
||||
_assetsCount = await tradeRepublicService.GetAssetsCount(stoppingToken);
|
||||
_currAssetsCount = new AssetsCount();
|
||||
|
||||
var initSettings = await settingsService.GetSettings();
|
||||
var isRecoveryMode = initSettings.CurrentScanningPage > 0;
|
||||
var initCurrentTypeStr = await settingsService.GetSettingAsync(SettingKeys.ScannerCurrentScanningType, stoppingToken);
|
||||
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 (isRecoveryMode)
|
||||
{
|
||||
if (type != initSettings.CurrentScanningType)
|
||||
if (type != initCurrentType)
|
||||
{
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Recovery: {AssetType} was already processed. Skipping.", type);
|
||||
continue;
|
||||
@@ -80,19 +93,17 @@ public class AssetScannerBackgroundService : BackgroundService
|
||||
}
|
||||
else
|
||||
{
|
||||
var settings = await settingsService.GetSettings();
|
||||
settings.CurrentScanningType = type;
|
||||
settings.CurrentScanningPage = 0;
|
||||
await settingsService.SaveSettings(settings);
|
||||
await settingsService.SetSettingAsync(SettingKeys.ScannerCurrentScanningType, type.ToString(), stoppingToken);
|
||||
await settingsService.SetSettingAsync(SettingKeys.ScannerCurrentScanningPage, 0, stoppingToken);
|
||||
}
|
||||
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Processing asset type: {AssetType}...", type);
|
||||
await HandleAssetType(type, tradeRepublicService, settingsService, assetsDbService, indexService, stoppingToken);
|
||||
|
||||
var currentSettings = await settingsService.GetSettings();
|
||||
var delaySeconds = currentSettings.FinishedInitialScan
|
||||
? currentSettings.AssetUpdateTypeDelay
|
||||
: currentSettings.InitAssetUpdateTypeDelay;
|
||||
var finishedInitial = await settingsService.GetSettingAsync(SettingKeys.ScannerFinishedInitialScan, stoppingToken);
|
||||
var delaySeconds = finishedInitial
|
||||
? await settingsService.GetSettingAsync(SettingKeys.ScannerTypeDelay, stoppingToken)
|
||||
: await settingsService.GetSettingAsync(SettingKeys.ScannerInitTypeDelay, stoppingToken);
|
||||
|
||||
if (delaySeconds > 0)
|
||||
{
|
||||
@@ -102,19 +113,18 @@ public class AssetScannerBackgroundService : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
var finalSettings = await settingsService.GetSettings();
|
||||
finalSettings.CurrentScanningPage = 0;
|
||||
await settingsService.SetSettingAsync(SettingKeys.ScannerCurrentScanningPage, 0, stoppingToken);
|
||||
|
||||
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.");
|
||||
finalSettings.FinishedInitialScan = true;
|
||||
await settingsService.SetSettingAsync(SettingKeys.ScannerFinishedInitialScan, true, stoppingToken);
|
||||
}
|
||||
|
||||
await settingsService.SaveSettings(finalSettings);
|
||||
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Full scan cycle completed. Waiting 1 minute before starting the next cycle.");
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||
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 Task.Delay(TimeSpan.FromMinutes(cycleDelay), stoppingToken);
|
||||
}
|
||||
catch (Exception e) when (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -129,7 +139,7 @@ public class AssetScannerBackgroundService : BackgroundService
|
||||
private async Task HandleAssetType(
|
||||
AssetType type,
|
||||
ITradeRepublicService tradeRepublicService,
|
||||
ISettingsDbService settingsDbService,
|
||||
ISettingsService settingsService,
|
||||
IAssetsDbService assetsDbService,
|
||||
IAssetsIndexService indexService,
|
||||
CancellationToken stoppingToken)
|
||||
@@ -144,26 +154,28 @@ public class AssetScannerBackgroundService : BackgroundService
|
||||
_currAssetsCount ??= new AssetsCount();
|
||||
var currentItemOffset = 0;
|
||||
|
||||
var settings = await settingsDbService.GetSettings();
|
||||
var pageSize = Math.Clamp(settings.TradeRepublicMaxRequestPageSize <= 0 ? 50 : settings.TradeRepublicMaxRequestPageSize, 1, 100);
|
||||
var configuredPageSize = await settingsService.GetSettingAsync(SettingKeys.ScannerMaxPageSize, stoppingToken);
|
||||
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}).",
|
||||
type, settings.CurrentScanningPage, currentItemOffset);
|
||||
type, currentScanningPage, currentItemOffset);
|
||||
}
|
||||
|
||||
while (currentItemOffset < totalCount && !stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
var currentSettings = await settingsDbService.GetSettings();
|
||||
pageSize = Math.Clamp(currentSettings.TradeRepublicMaxRequestPageSize <= 0 ? 50 : currentSettings.TradeRepublicMaxRequestPageSize, 1, 100);
|
||||
configuredPageSize = await settingsService.GetSettingAsync(SettingKeys.ScannerMaxPageSize, stoppingToken);
|
||||
pageSize = Math.Clamp(configuredPageSize <= 0 ? 50 : configuredPageSize, 1, 100);
|
||||
|
||||
var currentPage = (currentItemOffset / pageSize) + 1;
|
||||
|
||||
currentSettings.CurrentScanningType = type;
|
||||
currentSettings.CurrentScanningPage = currentPage;
|
||||
await settingsDbService.SaveSettings(currentSettings);
|
||||
await settingsService.SetSettingAsync(SettingKeys.ScannerCurrentScanningType, type.ToString(), stoppingToken);
|
||||
await settingsService.SetSettingAsync(SettingKeys.ScannerCurrentScanningPage, currentPage, stoppingToken);
|
||||
|
||||
await _finlyticLogger.LogDebugAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Fetching {AssetType} - Page {Page}. Numerical Offset: {Offset}/{Total}",
|
||||
type, currentPage, currentItemOffset, totalCount);
|
||||
@@ -173,8 +185,7 @@ public class AssetScannerBackgroundService : BackgroundService
|
||||
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);
|
||||
currentSettings.CurrentScanningPage = 0;
|
||||
await settingsDbService.SaveSettings(currentSettings);
|
||||
await settingsService.SetSettingAsync(SettingKeys.ScannerCurrentScanningPage, 0, stoppingToken);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -186,14 +197,14 @@ public class AssetScannerBackgroundService : BackgroundService
|
||||
if (assets.Results.Count < pageSize)
|
||||
{
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Reached the last page for {AssetType}.", type);
|
||||
currentSettings.CurrentScanningPage = 0;
|
||||
await settingsDbService.SaveSettings(currentSettings);
|
||||
await settingsService.SetSettingAsync(SettingKeys.ScannerCurrentScanningPage, 0, stoppingToken);
|
||||
break;
|
||||
}
|
||||
|
||||
var delaySeconds = currentSettings.FinishedInitialScan
|
||||
? currentSettings.BatchAssetUpdateDelay
|
||||
: currentSettings.InitBatchAssetUpdateDelay;
|
||||
var finishedInitial = await settingsService.GetSettingAsync(SettingKeys.ScannerFinishedInitialScan, stoppingToken);
|
||||
var delaySeconds = finishedInitial
|
||||
? await settingsService.GetSettingAsync(SettingKeys.ScannerBatchDelay, stoppingToken)
|
||||
: await settingsService.GetSettingAsync(SettingKeys.ScannerInitBatchDelay, stoppingToken);
|
||||
|
||||
if (delaySeconds > 0)
|
||||
{
|
||||
@@ -219,6 +230,26 @@ public class AssetScannerBackgroundService : BackgroundService
|
||||
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);
|
||||
|
||||
// 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)
|
||||
{
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetScannerBackgroundService] Database modifications detected. Recreating the asset index file...");
|
||||
|
||||
@@ -14,7 +14,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace FinlyticAssets.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Defines database operations for managing Trade Republic asset entities.
|
||||
/// Defines database operations for managing Trade Republic asset entities and on-demand derivatives.
|
||||
/// </summary>
|
||||
public interface IAssetsDbService
|
||||
{
|
||||
@@ -24,7 +24,6 @@ public interface IAssetsDbService
|
||||
public Task<List<AssetEntity>> GetAssetsByIsinAsync(string isin);
|
||||
public Task<List<AssetEntity>> GetValidAssetsByIsinAsync(string isin);
|
||||
public Task<List<AssetEntity>> FindAffectedActiveAssetsAsync(string searchQuery);
|
||||
public Task UpdateAssetImageIdAsync(string isin, string imageId);
|
||||
public Task<bool> DeleteAssetAsync(string isin);
|
||||
public Task<List<AssetEntity>> GetDiscoveryAssetsAsync(int limit = 15);
|
||||
public Task<List<DerivativeEntity>> GetDerivativesByUnderlyingAsync(string underlyingIsin, string optionType = "long", decimal? targetLeverage = null, string? after = null, int? page = null, bool forceRefresh = false, CancellationToken cancellationToken = default);
|
||||
@@ -63,7 +62,6 @@ public class AssetsDbService : IAssetsDbService
|
||||
Asset = a,
|
||||
Score = (a.Tags?.Count ?? 0) * 10
|
||||
+ (a.HasCfd ? 5 : 0)
|
||||
+ (string.IsNullOrEmpty(a.ImageId) ? 0 : 15)
|
||||
+ (a.Name.Length > 3 ? 5 : 0)
|
||||
})
|
||||
.OrderByDescending(x => x.Score)
|
||||
@@ -147,7 +145,6 @@ public class AssetsDbService : IAssetsDbService
|
||||
existingEntity.Type = dtoAsset.Type;
|
||||
existingEntity.InstrumentCategory = dtoAsset.InstrumentCategory;
|
||||
existingEntity.HasCfd = dtoAsset.HasCfd;
|
||||
existingEntity.ImageId = dtoAsset.ImageId;
|
||||
existingEntity.LastUpdatedAt = now;
|
||||
|
||||
UpdateSubtypeProperties(existingEntity, dtoAsset);
|
||||
@@ -220,14 +217,13 @@ public class AssetsDbService : IAssetsDbService
|
||||
existingEntity.Type != dto.Type ||
|
||||
existingEntity.InstrumentCategory != dto.InstrumentCategory ||
|
||||
existingEntity.HasCfd != dto.HasCfd ||
|
||||
existingEntity.ImageId != dto.ImageId ||
|
||||
!existingEntity.Tags.SequenceEqual(mappedTags))
|
||||
!existingEntity.Tags.Select(t => t.Id).Order().SequenceEqual(mappedTags.Select(t => t.Id).Order()) ||
|
||||
HasSubtypeChanges(existingEntity, dto))
|
||||
{
|
||||
existingEntity.Name = dto.Name;
|
||||
existingEntity.Type = dto.Type;
|
||||
existingEntity.InstrumentCategory = dto.InstrumentCategory;
|
||||
existingEntity.HasCfd = dto.HasCfd;
|
||||
existingEntity.ImageId = dto.ImageId;
|
||||
existingEntity.LastUpdatedAt = now;
|
||||
existingEntity.Tags = mappedTags;
|
||||
|
||||
@@ -255,18 +251,8 @@ public class AssetsDbService : IAssetsDbService
|
||||
Type = stock.Type,
|
||||
InstrumentCategory = stock.InstrumentCategory,
|
||||
HasCfd = stock.HasCfd,
|
||||
ImageId = stock.ImageId,
|
||||
DerivativeProductCategories = stock.DerivativeProductCategories?.ToList() ?? new List<string>()
|
||||
},
|
||||
TradeRepublicCrypto crypto => new CryptoEntity
|
||||
{
|
||||
Isin = crypto.Isin,
|
||||
Name = crypto.Name,
|
||||
Type = crypto.Type,
|
||||
InstrumentCategory = crypto.InstrumentCategory,
|
||||
HasCfd = crypto.HasCfd,
|
||||
ImageId = crypto.ImageId
|
||||
},
|
||||
TradeRepublicEtf etf => new EtfEntity
|
||||
{
|
||||
Isin = etf.Isin,
|
||||
@@ -274,7 +260,6 @@ public class AssetsDbService : IAssetsDbService
|
||||
Type = etf.Type,
|
||||
InstrumentCategory = etf.InstrumentCategory,
|
||||
HasCfd = etf.HasCfd,
|
||||
ImageId = etf.ImageId,
|
||||
DerivativeProductCategories = etf.DerivativeProductCategories?.ToList() ?? new List<string>()
|
||||
},
|
||||
TradeRepublicSynthetic syn => new SyntheticEntity
|
||||
@@ -284,39 +269,15 @@ public class AssetsDbService : IAssetsDbService
|
||||
Type = syn.Type,
|
||||
InstrumentCategory = syn.InstrumentCategory,
|
||||
HasCfd = syn.HasCfd,
|
||||
ImageId = syn.ImageId,
|
||||
DerivativeProductCategories = syn.DerivativeProductCategories?.ToList() ?? new List<string>()
|
||||
},
|
||||
TradeRepublicBond bond => new BondEntity
|
||||
{
|
||||
Isin = bond.Isin,
|
||||
Name = bond.Name,
|
||||
Type = bond.Type,
|
||||
InstrumentCategory = bond.InstrumentCategory,
|
||||
HasCfd = bond.HasCfd,
|
||||
ImageId = bond.ImageId,
|
||||
BondIssuerName = bond.BondIssuerName,
|
||||
SearchSubtitle = bond.SearchSubtitle
|
||||
},
|
||||
TradeRepublicDerivative deriv => new DerivativeEntity
|
||||
{
|
||||
Isin = deriv.Isin,
|
||||
Name = deriv.Name,
|
||||
Type = deriv.Type,
|
||||
InstrumentCategory = deriv.InstrumentCategory,
|
||||
HasCfd = deriv.HasCfd,
|
||||
ImageId = deriv.ImageId,
|
||||
UnderlyingIsin = deriv.UnderlyingIsin,
|
||||
DerivativeProductCategories = deriv.DerivativeProductCategories?.ToList() ?? new List<string>()
|
||||
},
|
||||
_ => new StockEntity
|
||||
{
|
||||
Isin = dto.Isin,
|
||||
Name = dto.Name,
|
||||
Type = dto.Type,
|
||||
InstrumentCategory = dto.InstrumentCategory,
|
||||
HasCfd = dto.HasCfd,
|
||||
ImageId = dto.ImageId
|
||||
HasCfd = dto.HasCfd
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -334,14 +295,21 @@ public class AssetsDbService : IAssetsDbService
|
||||
case SyntheticEntity syn when dto is TradeRepublicSynthetic synDto:
|
||||
syn.DerivativeProductCategories = synDto.DerivativeProductCategories?.ToList() ?? new List<string>();
|
||||
break;
|
||||
case BondEntity bond when dto is TradeRepublicBond b:
|
||||
bond.BondIssuerName = b.BondIssuerName;
|
||||
bond.SearchSubtitle = b.SearchSubtitle;
|
||||
break;
|
||||
case DerivativeEntity deriv when dto is TradeRepublicDerivative d:
|
||||
deriv.UnderlyingIsin = d.UnderlyingIsin;
|
||||
deriv.DerivativeProductCategories = d.DerivativeProductCategories?.ToList() ?? new List<string>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasSubtypeChanges(AssetEntity entity, TradeRepublicAsset dto)
|
||||
{
|
||||
switch (entity)
|
||||
{
|
||||
case StockEntity stock when dto is TradeRepublicStock s:
|
||||
return !(stock.DerivativeProductCategories?.SequenceEqual(s.DerivativeProductCategories ?? Array.Empty<string>()) ?? (s.DerivativeProductCategories == null || !s.DerivativeProductCategories.Any()));
|
||||
case EtfEntity etf when dto is TradeRepublicEtf e:
|
||||
return !(etf.DerivativeProductCategories?.SequenceEqual(e.DerivativeProductCategories ?? Array.Empty<string>()) ?? (e.DerivativeProductCategories == null || !e.DerivativeProductCategories.Any()));
|
||||
case SyntheticEntity syn when dto is TradeRepublicSynthetic synDto:
|
||||
return !(syn.DerivativeProductCategories?.SequenceEqual(synDto.DerivativeProductCategories ?? Array.Empty<string>()) ?? (synDto.DerivativeProductCategories == null || !synDto.DerivativeProductCategories.Any()));
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,24 +330,6 @@ public class AssetsDbService : IAssetsDbService
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>Inherits documentation from interface.</summary>
|
||||
public async Task UpdateAssetImageIdAsync(string isin, string imageId)
|
||||
{
|
||||
var existingAssets = await _context.TradeRepublicAssets
|
||||
.Where(a => a.Isin == isin)
|
||||
.ToListAsync();
|
||||
|
||||
if (existingAssets.Count > 0)
|
||||
{
|
||||
foreach (var asset in existingAssets)
|
||||
{
|
||||
asset.ImageId = imageId;
|
||||
}
|
||||
await _context.SaveChangesAsync();
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetsDbService] Updated ImageId for ISIN {Isin} in database to '{ImageId}'", isin, imageId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Inherits documentation from interface.</summary>
|
||||
public async Task<bool> DeleteAssetAsync(string isin)
|
||||
{
|
||||
@@ -412,7 +362,6 @@ public class AssetsDbService : IAssetsDbService
|
||||
int pageIndex = Math.Max(0, page ?? 0);
|
||||
|
||||
decimal levQuery = targetLeverage.HasValue && targetLeverage.Value > 0 ? targetLeverage.Value : 0m;
|
||||
|
||||
string trAfter = !string.IsNullOrEmpty(after) ? after : (pageIndex > 0 ? pageIndex.ToString() : "0");
|
||||
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetsDbService] Fetching derivatives for {Isin} (OptionType: {Option}, Leverage: {Lev}, Page: {Page}, TR-After: {After})",
|
||||
@@ -438,8 +387,7 @@ public class AssetsDbService : IAssetsDbService
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var isins = fetchedItems.Select(r => r.Isin).ToList();
|
||||
var existingDerivatives = await _context.TradeRepublicAssets
|
||||
.OfType<DerivativeEntity>()
|
||||
var existingDerivatives = await _context.Derivatives
|
||||
.Where(d => isins.Contains(d.Isin))
|
||||
.ToDictionaryAsync(d => d.Isin, cancellationToken);
|
||||
|
||||
@@ -466,14 +414,13 @@ public class AssetsDbService : IAssetsDbService
|
||||
existing.NextGenProductCategoryName = item.NextGenProductCategoryName;
|
||||
existing.Issuer = item.Issuer;
|
||||
existing.IssuerDisplayName = item.IssuerDisplayName;
|
||||
existing.IssuerImageId = item.IssuerImageId;
|
||||
existing.Size = item.Size;
|
||||
existing.Factor = item.Factor;
|
||||
existing.Delta = item.Delta;
|
||||
existing.Currency = item.Currency;
|
||||
existing.LastUpdatedAt = now;
|
||||
|
||||
_context.TradeRepublicAssets.Update(existing);
|
||||
_context.Derivatives.Update(existing);
|
||||
resultEntities.Add(existing);
|
||||
}
|
||||
else
|
||||
@@ -482,8 +429,6 @@ public class AssetsDbService : IAssetsDbService
|
||||
{
|
||||
Isin = item.Isin,
|
||||
Name = !string.IsNullOrWhiteSpace(item.ProductCategoryName) ? item.ProductCategoryName : item.Isin,
|
||||
Type = "derivative",
|
||||
InstrumentCategory = "derivative",
|
||||
UnderlyingIsin = underlyingIsin,
|
||||
Strike = item.Strike ?? 0m,
|
||||
Barrier = item.Barrier ?? 0m,
|
||||
@@ -494,27 +439,33 @@ public class AssetsDbService : IAssetsDbService
|
||||
NextGenProductCategoryName = item.NextGenProductCategoryName,
|
||||
Issuer = item.Issuer,
|
||||
IssuerDisplayName = item.IssuerDisplayName,
|
||||
IssuerImageId = item.IssuerImageId,
|
||||
Size = item.Size,
|
||||
Factor = item.Factor,
|
||||
Delta = item.Delta,
|
||||
Currency = item.Currency,
|
||||
LastUpdatedAt = now
|
||||
LastUpdatedAt = now,
|
||||
CreatedAt = now
|
||||
};
|
||||
|
||||
await _context.TradeRepublicAssets.AddAsync(newDeriv, cancellationToken);
|
||||
await _context.Derivatives.AddAsync(newDeriv, cancellationToken);
|
||||
resultEntities.Add(newDeriv);
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return resultEntities;
|
||||
return resultEntities.Where(d => d.Barrier > 0 && d.Leverage > 0).ToList();
|
||||
}
|
||||
|
||||
return await _context.TradeRepublicAssets
|
||||
.OfType<DerivativeEntity>()
|
||||
var dbQuery = _context.Derivatives
|
||||
.AsNoTracking()
|
||||
.Where(d => d.UnderlyingIsin == underlyingIsin)
|
||||
.Where(d => d.UnderlyingIsin == underlyingIsin && d.OptionType == targetOptionType && d.Barrier > 0 && d.Leverage > 0);
|
||||
|
||||
if (targetLeverage.HasValue && targetLeverage.Value > 0)
|
||||
{
|
||||
dbQuery = dbQuery.OrderBy(d => Math.Abs(d.Leverage - targetLeverage.Value));
|
||||
}
|
||||
|
||||
return await dbQuery
|
||||
.Take(pageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticAssets.Models;
|
||||
using FinlyticCore.Models.Assets;
|
||||
using FinlyticAssets.Util;
|
||||
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 FinlyticCore.Dtos.Assets;
|
||||
|
||||
@@ -14,6 +17,8 @@ public static class AssetMapper
|
||||
Type = t.Type
|
||||
}).ToList();
|
||||
|
||||
var dynamicLogoUrl = $"/api/v1/logo/{entity.Isin}";
|
||||
|
||||
return entity switch
|
||||
{
|
||||
StockEntity stock => new StockDto
|
||||
@@ -23,7 +28,7 @@ public static class AssetMapper
|
||||
Type = stock.Type,
|
||||
InstrumentCategory = stock.InstrumentCategory,
|
||||
HasCfd = stock.HasCfd,
|
||||
ImageId = stock.ImageId,
|
||||
ImageId = dynamicLogoUrl,
|
||||
LastUpdatedAt = stock.LastUpdatedAt,
|
||||
Tags = dtoTags,
|
||||
DerivativeProductCategories = stock.DerivativeProductCategories
|
||||
@@ -35,7 +40,7 @@ public static class AssetMapper
|
||||
Type = etf.Type,
|
||||
InstrumentCategory = etf.InstrumentCategory,
|
||||
HasCfd = etf.HasCfd,
|
||||
ImageId = etf.ImageId,
|
||||
ImageId = dynamicLogoUrl,
|
||||
LastUpdatedAt = etf.LastUpdatedAt,
|
||||
Tags = dtoTags,
|
||||
DerivativeProductCategories = etf.DerivativeProductCategories,
|
||||
@@ -44,59 +49,6 @@ public static class AssetMapper
|
||||
Subtitle = etf.Subtitle,
|
||||
SearchSubtitle = etf.SearchSubtitle
|
||||
},
|
||||
CryptoEntity crypto => new CryptoDto
|
||||
{
|
||||
Isin = crypto.Isin,
|
||||
Name = crypto.Name,
|
||||
Type = crypto.Type,
|
||||
InstrumentCategory = crypto.InstrumentCategory,
|
||||
HasCfd = crypto.HasCfd,
|
||||
ImageId = crypto.ImageId,
|
||||
LastUpdatedAt = crypto.LastUpdatedAt,
|
||||
Tags = dtoTags,
|
||||
Subtitle = crypto.Subtitle,
|
||||
SearchSubtitle = crypto.SearchSubtitle
|
||||
},
|
||||
BondEntity bond => new BondDto
|
||||
{
|
||||
Isin = bond.Isin,
|
||||
Name = bond.Name,
|
||||
Type = bond.Type,
|
||||
InstrumentCategory = bond.InstrumentCategory,
|
||||
HasCfd = bond.HasCfd,
|
||||
ImageId = bond.ImageId,
|
||||
LastUpdatedAt = bond.LastUpdatedAt,
|
||||
Tags = dtoTags,
|
||||
BondIssuerName = bond.BondIssuerName,
|
||||
SearchSubtitle = bond.SearchSubtitle
|
||||
},
|
||||
DerivativeEntity deriv => new DerivativeDto
|
||||
{
|
||||
Isin = deriv.Isin,
|
||||
Name = deriv.Name,
|
||||
Type = deriv.Type,
|
||||
InstrumentCategory = deriv.InstrumentCategory,
|
||||
HasCfd = deriv.HasCfd,
|
||||
ImageId = deriv.ImageId,
|
||||
LastUpdatedAt = deriv.LastUpdatedAt,
|
||||
Tags = dtoTags,
|
||||
DerivativeProductCategories = deriv.DerivativeProductCategories,
|
||||
UnderlyingIsin = deriv.UnderlyingIsin,
|
||||
OptionType = deriv.OptionType.ToString(),
|
||||
ProductCategoryName = deriv.ProductCategoryName,
|
||||
NextGenProductCategoryName = deriv.NextGenProductCategoryName,
|
||||
Strike = deriv.Strike,
|
||||
Barrier = deriv.Barrier,
|
||||
Leverage = deriv.Leverage,
|
||||
Size = deriv.Size,
|
||||
Factor = deriv.Factor,
|
||||
Delta = deriv.Delta,
|
||||
Currency = deriv.Currency,
|
||||
Expiry = deriv.Expiry,
|
||||
Issuer = deriv.Issuer,
|
||||
IssuerDisplayName = deriv.IssuerDisplayName,
|
||||
IssuerImageId = deriv.IssuerImageId
|
||||
},
|
||||
SyntheticEntity synth => new SyntheticDto
|
||||
{
|
||||
Isin = synth.Isin,
|
||||
@@ -104,12 +56,53 @@ public static class AssetMapper
|
||||
Type = synth.Type,
|
||||
InstrumentCategory = synth.InstrumentCategory,
|
||||
HasCfd = synth.HasCfd,
|
||||
ImageId = synth.ImageId,
|
||||
ImageId = dynamicLogoUrl,
|
||||
LastUpdatedAt = synth.LastUpdatedAt,
|
||||
Tags = dtoTags,
|
||||
DerivativeProductCategories = synth.DerivativeProductCategories
|
||||
},
|
||||
_ => throw new NotSupportedException($"Mapping for type {entity.GetType().Name} is not supported.")
|
||||
_ => new StockDto
|
||||
{
|
||||
Isin = entity.Isin,
|
||||
Name = entity.Name,
|
||||
Type = entity.Type,
|
||||
InstrumentCategory = entity.InstrumentCategory,
|
||||
HasCfd = entity.HasCfd,
|
||||
ImageId = dynamicLogoUrl,
|
||||
LastUpdatedAt = entity.LastUpdatedAt,
|
||||
Tags = dtoTags
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static DerivativeDto ToDto(this DerivativeEntity deriv)
|
||||
{
|
||||
return new DerivativeDto
|
||||
{
|
||||
Isin = deriv.Isin,
|
||||
Name = deriv.Name,
|
||||
Type = "derivative",
|
||||
InstrumentCategory = "derivative",
|
||||
HasCfd = false,
|
||||
ImageId = $"/api/v1/logo/{deriv.UnderlyingIsin}",
|
||||
LastUpdatedAt = deriv.LastUpdatedAt,
|
||||
Tags = new List<TagDto>(),
|
||||
DerivativeProductCategories = deriv.DerivativeProductCategories,
|
||||
UnderlyingIsin = deriv.UnderlyingIsin,
|
||||
OptionType = deriv.OptionType.ToString(),
|
||||
ProductCategoryName = deriv.ProductCategoryName,
|
||||
NextGenProductCategoryName = deriv.NextGenProductCategoryName,
|
||||
Strike = deriv.Strike,
|
||||
Barrier = deriv.Barrier,
|
||||
Leverage = deriv.Leverage,
|
||||
Size = deriv.Size,
|
||||
Factor = deriv.Factor,
|
||||
Delta = deriv.Delta,
|
||||
Currency = deriv.Currency,
|
||||
Expiry = deriv.Expiry,
|
||||
Issuer = deriv.Issuer,
|
||||
IssuerDisplayName = deriv.IssuerDisplayName,
|
||||
IssuerImageId = null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -117,4 +110,9 @@ public static class AssetMapper
|
||||
{
|
||||
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.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
@@ -6,9 +7,14 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticAssets.Entities;
|
||||
using FinlyticAssets.Services;
|
||||
using FinlyticCore.Dtos;
|
||||
using FinlyticCore.Dtos.Assets;
|
||||
using FinlyticCore.Dtos.Settings;
|
||||
using FinlyticCore.Dtos.TechnicalAnalysis;
|
||||
using FinlyticCore.Models;
|
||||
using FinlyticCore.Models.Assets;
|
||||
using FinlyticCore.Services;
|
||||
using FinlyticCore.Services.TradeRepublic;
|
||||
using FinlyticCore.Util;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -18,13 +24,14 @@ using Microsoft.Extensions.Logging;
|
||||
namespace FinlyticAssets.Util;
|
||||
|
||||
/// <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>
|
||||
public class AssetsMqttClient : ManagedMqttClient, IHostedService
|
||||
{
|
||||
private readonly ILogger<AssetsMqttClient> _logger;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ConcurrentDictionary<string, (LivePriceDto Price, DateTime CachedAt)> _priceCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public AssetsMqttClient(
|
||||
ILogger<AssetsMqttClient> logger,
|
||||
@@ -41,12 +48,7 @@ public class AssetsMqttClient : ManagedMqttClient, IHostedService
|
||||
/// </summary>
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var config = new MqttConfiguration()
|
||||
{
|
||||
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()}"
|
||||
};
|
||||
var config = MqttConfiguration.FromConfiguration(_configuration, "FinlyticAssets");
|
||||
|
||||
_logger.LogInformation("Starting Assets MQTT client. Host: {Host}, ClientId: {ClientId}", config.Host, config.ClientId);
|
||||
await ConnectAsync(config);
|
||||
@@ -66,278 +68,158 @@ public class AssetsMqttClient : ManagedMqttClient, IHostedService
|
||||
/// </summary>
|
||||
protected override async Task OnConnectedAsync()
|
||||
{
|
||||
_logger.LogInformation("Assets MQTT Client connected. Subscribing to topics...");
|
||||
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/#");
|
||||
_logger.LogInformation("Assets MQTT Client connected. Registering topic subscriptions...");
|
||||
|
||||
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))
|
||||
{
|
||||
await PublishAsync("finlytic/logs/FinlyticAssets", logDto);
|
||||
await PublishAsync(MqttTopics.Logs("FinlyticAssets"), logDto);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes incoming messages on the subscribed topics.
|
||||
/// </summary>
|
||||
protected override async Task OnMessageReceivedAsync(string topic, string payload)
|
||||
private async Task<List<AssetDto>> HandleAssetsGetRpcAsync(GetValidAssetRequest? req, string correlationId)
|
||||
{
|
||||
if (topic.StartsWith("services/config/updated", StringComparison.OrdinalIgnoreCase))
|
||||
if (req == null || string.IsNullOrWhiteSpace(req.Isin)) return [];
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var dbService = scope.ServiceProvider.GetRequiredService<IAssetsDbService>();
|
||||
var assets = await dbService.GetValidAssetsByIsinAsync(req.Isin.Trim().ToUpperInvariant());
|
||||
return assets.ToDtoList();
|
||||
}
|
||||
|
||||
private async Task<List<AssetDto>> HandleAssetsGetDiscoveryRpcAsync(GetDiscoveryAssetsRequest? req, string correlationId)
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var dbService = scope.ServiceProvider.GetRequiredService<IAssetsDbService>();
|
||||
int limit = req?.Limit > 0 ? req.Limit : 15;
|
||||
var discoveryAssets = await dbService.GetDiscoveryAssetsAsync(limit);
|
||||
return discoveryAssets.ToDtoList();
|
||||
}
|
||||
|
||||
private async Task<List<DerivativeDto>> HandleAssetsGetDerivativesRpcAsync(GetDerivativesRequest? req, string correlationId)
|
||||
{
|
||||
if (req == null || string.IsNullOrWhiteSpace(req.UnderlyingIsin)) return [];
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var dbService = scope.ServiceProvider.GetRequiredService<IAssetsDbService>();
|
||||
var derivatives = await dbService.GetDerivativesByUnderlyingAsync(
|
||||
req.UnderlyingIsin.Trim().ToUpperInvariant(),
|
||||
req.OptionType,
|
||||
req.TargetLeverage,
|
||||
req.After,
|
||||
req.Page,
|
||||
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)
|
||||
{
|
||||
await HandleConfigUpdatedAsync(topic, payload);
|
||||
return;
|
||||
return cached.Price;
|
||||
}
|
||||
|
||||
var segments = topic.Split('/');
|
||||
if (segments.Length < 4) return;
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var trService = scope.ServiceProvider.GetRequiredService<ITradeRepublicService>();
|
||||
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<AssetsMqttClient>>();
|
||||
|
||||
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;
|
||||
}
|
||||
var tcs = new TaskCompletionSource<LivePriceDto?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
int? subId = null;
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var dbService = scope.ServiceProvider.GetRequiredService<IAssetsDbService>();
|
||||
var indexService = scope.ServiceProvider.GetRequiredService<IAssetsIndexService>();
|
||||
|
||||
if (channel == "assets_FetchLogo")
|
||||
subId = await trService.SubscribeRealtimeTickerAsync(cleanIsin, tick =>
|
||||
{
|
||||
await HandleFetchLogoAsync(payload, correlationId, indexService);
|
||||
return;
|
||||
}
|
||||
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));
|
||||
|
||||
List<AssetEntity> responseData = [];
|
||||
decimal preClose = tick.Pre?.PriceValue > 0 ? tick.Pre.PriceValue : (tick.Open?.PriceValue ?? 0m);
|
||||
decimal dailyChange = preClose > 0m ? ((currentPrice - preClose) / preClose) * 100m : 0m;
|
||||
|
||||
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;
|
||||
}
|
||||
var livePrice = new LivePriceDto(
|
||||
Isin: cleanIsin,
|
||||
CurrentPrice: currentPrice,
|
||||
DailyChangePercent: dailyChange,
|
||||
Bid: tick.Bid?.PriceValue,
|
||||
Ask: tick.Ask?.PriceValue
|
||||
);
|
||||
|
||||
string defaultResponseTopic = $"services/response/{channel}/{correlationId}";
|
||||
await PublishAsync(defaultResponseTopic, responseData.ToDtoList());
|
||||
_priceCache[cleanIsin] = (livePrice, DateTime.UtcNow);
|
||||
tcs.TrySetResult(livePrice);
|
||||
});
|
||||
|
||||
var result = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(3));
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnError(ex);
|
||||
await finlyticLogger.LogWarningAsync(SettingKeys.AssetsChannel, "[AssetsMqttClient] Failed or timed out fetching live price for {Isin}: {Message}", cleanIsin, ex.Message);
|
||||
if (_priceCache.TryGetValue(cleanIsin, out var stale))
|
||||
{
|
||||
return stale.Price;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (subId.HasValue)
|
||||
{
|
||||
_ = trService.UnsubscribeRealtimeTickerAsync(subId.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleSettingsGetAllAsync(string correlationId)
|
||||
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 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.");
|
||||
}
|
||||
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 HandleSettingsUpdateAsync(string payload, string correlationId)
|
||||
private async Task<List<DynamicSettingDto>> HandleSettingsUpdateRpcAsync(Dictionary<string, object?>? updates, string correlationId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(payload)) return;
|
||||
|
||||
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);
|
||||
try
|
||||
if (updates != null && updates.Count > 0)
|
||||
{
|
||||
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.");
|
||||
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 HandleConfigUpdatedAsync(string topic, string payload)
|
||||
private async Task HandleHealthPingRpcAsync(object? _, string topic, string correlationId)
|
||||
{
|
||||
if (!topic.EndsWith("FinlyticAssets", StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
try
|
||||
if (topic.Contains("FinlyticAssets", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
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}";
|
||||
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 live health_Ping RPC request [CorrelationId: {CorrelationId}].", correlationId);
|
||||
await finlyticLogger.LogInfoAsync(SettingKeys.HealthPingChannel, "[AssetsMqttClient] Responded to 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.TargetLeverage,
|
||||
req.After,
|
||||
req.Page,
|
||||
req.ShouldForceRefresh);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<AssetsMqttClient>>();
|
||||
await finlyticLogger.LogErrorAsync(SettingKeys.AssetsChannel, ex, "[AssetsMqttClient] Error parsing GetDerivativesRequest payload.");
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,21 @@ public static class SettingKeys
|
||||
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> HealthPingChannel = new("Logging.Channel.Health", true);
|
||||
public static readonly SettingKey<bool> TradeRepublicChannel = new("Logging.Channel.TradeRepublic", true);
|
||||
|
||||
// --- Asset Scanning ---
|
||||
public static readonly SettingKey<bool> EnableAutoScan = new("Scanner.EnableAutoScan", true);
|
||||
public static readonly SettingKey<int> ScanIntervalHours = new("Scanner.ScanIntervalHours", 12);
|
||||
public static readonly SettingKey<int> MaxConcurrentScans = new("Scanner.MaxConcurrentScans", 5);
|
||||
public static readonly SettingKey<bool> EnableDerivativeScanning = new("Scanner.EnableDerivativeScanning", true);
|
||||
// --- Trade Republic Connection ---
|
||||
public static readonly SettingKey<int> TradeRepublicWsReconnectInterval = new("TradeRepublic.WsReconnectIntervalSeconds", 5);
|
||||
public static readonly SettingKey<int> TradeRepublicWsTimeout = new("TradeRepublic.WsTimeoutSeconds", 15);
|
||||
|
||||
// --- Logos & Media ---
|
||||
public static readonly SettingKey<bool> AutoFetchLogos = new("Media.AutoFetchLogos", true);
|
||||
public static readonly SettingKey<int> LogoFetchBatchSize = new("Media.LogoFetchBatchSize", 25);
|
||||
public static readonly SettingKey<string> LogoStorageDirectory = new("Media.LogoStorageDirectory", "data/logos");
|
||||
// --- Asset Scanning Config ---
|
||||
public static readonly SettingKey<bool> ScannerEnableAutoScan = new("Scanner.EnableAutoScan", true);
|
||||
public static readonly SettingKey<string> ScannerCurrentScanningType = new("Scanner.CurrentScanningType", "Stock");
|
||||
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