feat(trades): dynamic settings, IFinlyticLogger, live log streaming, and EF migration
This commit is contained in:
@@ -1,10 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FinlyticCore.Database;
|
||||||
using FinlyticCore.Entities.Settings;
|
using FinlyticCore.Entities.Settings;
|
||||||
using FinlyticTrades.Entities;
|
using FinlyticTrades.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
|
||||||
namespace FinlyticTrades.Database;
|
namespace FinlyticTrades.Database;
|
||||||
|
|
||||||
public class TradesDbContext : DbContext
|
public class TradesDbContext : DbContext, ISettingsDbContext
|
||||||
{
|
{
|
||||||
public TradesDbContext(DbContextOptions<TradesDbContext> options) : base(options) { }
|
public TradesDbContext(DbContextOptions<TradesDbContext> options) : base(options) { }
|
||||||
|
|
||||||
@@ -20,7 +25,7 @@ public class TradesDbContext : DbContext
|
|||||||
modelBuilder.Entity<SettingEntity>(entity =>
|
modelBuilder.Entity<SettingEntity>(entity =>
|
||||||
{
|
{
|
||||||
entity.HasKey(e => e.Id);
|
entity.HasKey(e => e.Id);
|
||||||
entity.HasIndex(e => e.Key);
|
entity.HasIndex(e => e.Key).IsUnique();
|
||||||
});
|
});
|
||||||
|
|
||||||
var stringListConverter =
|
var stringListConverter =
|
||||||
@@ -57,3 +62,13 @@ public class TradesDbContext : DbContext
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class TradesDbContextFactory : IDesignTimeDbContextFactory<TradesDbContext>
|
||||||
|
{
|
||||||
|
public TradesDbContext CreateDbContext(string[] args)
|
||||||
|
{
|
||||||
|
var optionsBuilder = new DbContextOptionsBuilder<TradesDbContext>();
|
||||||
|
optionsBuilder.UseNpgsql("Host=localhost;Database=trades;Username=postgres;Password=postgres");
|
||||||
|
return new TradesDbContext(optionsBuilder.Options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,358 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FinlyticTrades.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 FinlyticTrades.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(TradesDbContext))]
|
||||||
|
[Migration("20260815184034_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("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("FinlyticTrades.Entities.TradeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ActualEntryPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("AnalysisId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("AssetType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("CloseReason")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ClosedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("CompanyName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(150)
|
||||||
|
.HasColumnType("character varying(150)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("DerivativeIsin")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<string>("DerivativeProductCategories")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EntryFee")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("EntryPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EntryZoneMax")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EntryZoneMin")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("EventId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExecutionTimestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ExitFee")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("FundamentalRationale")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("HasCfd")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("InstrumentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsGlobalProposal")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRecurring")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool?>("IsWin")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("KnockoutThreshold")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("LeverageUsed")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("MaxLeverage")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PnlAbsolute")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PnlPercent")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PositionSize")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Quantity")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Reasoning")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal?>("RiskRewardRatio")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("RiskTolerance")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<string>("RiskWarning")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Sector")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("SignalType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("character varying(10)");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<decimal>("StopLoss")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Symbol")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<decimal>("TakeProfit")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("TakeProfitTargets")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("TechnicalRationale")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Timeframe")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<string>("TradeId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<int>("TtlMinutes")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<decimal?>("UserExitPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UserExitTimestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<int>("VixRegime")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<decimal>("VixValue")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<double>("WinRate")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AnalysisId");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("EventId");
|
||||||
|
|
||||||
|
b.HasIndex("Isin");
|
||||||
|
|
||||||
|
b.HasIndex("Sector");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.HasIndex("TradeId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("trades");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal>("CurrentPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("FloatingPnlPercent")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Reasoning")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Recommendation")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("character varying(30)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("SuggestedStopLoss")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("SuggestedTakeProfit")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Timestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("TradeId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal>("VixValue")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Timestamp");
|
||||||
|
|
||||||
|
b.HasIndex("TradeId");
|
||||||
|
|
||||||
|
b.HasIndex("TradeId", "Timestamp");
|
||||||
|
|
||||||
|
b.ToTable("trade_hourly_updates");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticTrades.Entities.TradesSettingsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<double>("AtrStopLossMultiplier")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<int>("MaxOpenPositions")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<double>("RiskPerTradePercentage")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FinlyticTrades.Entities.TradeEntity", "Trade")
|
||||||
|
.WithMany("HourlyUpdates")
|
||||||
|
.HasForeignKey("TradeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Trade");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("HourlyUpdates");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FinlyticTrades.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddDynamicSettings : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,8 @@ namespace FinlyticTrades.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("Key");
|
b.HasIndex("Key")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("DynamicSettings");
|
b.ToTable("DynamicSettings");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using FinlyticCore.Models.Trades;
|
using FinlyticCore.Database;
|
||||||
|
using FinlyticCore.Services;
|
||||||
using FinlyticTrades.Database;
|
using FinlyticTrades.Database;
|
||||||
using FinlyticTrades.Services;
|
using FinlyticTrades.Services;
|
||||||
using FinlyticTrades.Util;
|
using FinlyticTrades.Util;
|
||||||
@@ -7,19 +8,23 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
var builder = Host.CreateApplicationBuilder(args);
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
|
||||||
// 1. Standard DbContext (Scoped)
|
// 1. Standard DbContext (Scoped)
|
||||||
builder.Services.AddDbContext<TradesDbContext>(options =>
|
builder.Services.AddDbContext<TradesDbContext>(options =>
|
||||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||||
|
builder.Services.AddScoped<ISettingsDbContext>(sp => sp.GetRequiredService<TradesDbContext>());
|
||||||
|
|
||||||
// 2. Domain Services (Scoped)
|
// 2. Core Services
|
||||||
|
builder.Services.AddSingleton<ISettingsService, SettingsService>();
|
||||||
|
builder.Services.AddSingleton(typeof(IFinlyticLogger<>), typeof(FinlyticLogger<>));
|
||||||
|
|
||||||
|
// 3. Domain Services (Scoped)
|
||||||
builder.Services.AddScoped<ITradeLifecycleService, TradeLifecycleService>();
|
builder.Services.AddScoped<ITradeLifecycleService, TradeLifecycleService>();
|
||||||
builder.Services.AddScoped<ISettingsDbService, SettingsDbService>();
|
builder.Services.AddScoped<ISettingsDbService, SettingsDbService>();
|
||||||
|
|
||||||
// 3. Hosted Services / Singletons
|
// 4. Hosted Services / Singletons
|
||||||
builder.Services.AddSingleton<TradesMqttClient>();
|
builder.Services.AddSingleton<TradesMqttClient>();
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<TradesMqttClient>());
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<TradesMqttClient>());
|
||||||
builder.Services.AddHostedService<FeedbackExporterEngine>();
|
builder.Services.AddHostedService<FeedbackExporterEngine>();
|
||||||
@@ -34,14 +39,10 @@ using (var scope = host.Services.CreateScope())
|
|||||||
var context = scope.ServiceProvider.GetRequiredService<TradesDbContext>();
|
var context = scope.ServiceProvider.GetRequiredService<TradesDbContext>();
|
||||||
await context.Database.MigrateAsync();
|
await context.Database.MigrateAsync();
|
||||||
Console.WriteLine("Database migrations successfully executed for FinlyticTrades.");
|
Console.WriteLine("Database migrations successfully executed for FinlyticTrades.");
|
||||||
|
|
||||||
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
|
|
||||||
await settingsService.GetSettingsAsync();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
|
Console.WriteLine($"Critical error during database migration for FinlyticTrades: {ex.Message}");
|
||||||
logger.LogError(ex, "An error occurred during database migration for FinlyticTrades on startup.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ using System.Text.RegularExpressions;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FinlyticCore.Models.Trades;
|
using FinlyticCore.Models.Trades;
|
||||||
|
using FinlyticCore.Services;
|
||||||
using FinlyticTrades.Database;
|
using FinlyticTrades.Database;
|
||||||
using FinlyticTrades.Entities;
|
using FinlyticTrades.Entities;
|
||||||
|
using FinlyticTrades.Util;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Parquet.Serialization;
|
using Parquet.Serialization;
|
||||||
|
|
||||||
namespace FinlyticTrades.Services;
|
namespace FinlyticTrades.Services;
|
||||||
@@ -25,17 +26,16 @@ public interface IFeedbackExporterEngine
|
|||||||
Task ExportFeedbackDataAsync(CancellationToken cancellationToken = default);
|
Task ExportFeedbackDataAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class FeedbackExporterEngine : BackgroundService, IFeedbackExporterEngine
|
public class FeedbackExporterEngine : BackgroundService, IFeedbackExporterEngine
|
||||||
{
|
{
|
||||||
private readonly IServiceScopeFactory _scopeFactory;
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
private readonly ILogger<FeedbackExporterEngine> _logger;
|
private readonly IFinlyticLogger<FeedbackExporterEngine> _finlyticLogger;
|
||||||
private readonly string _feedbackDir;
|
private readonly string _feedbackDir;
|
||||||
|
|
||||||
public FeedbackExporterEngine(IServiceScopeFactory scopeFactory, ILogger<FeedbackExporterEngine> logger)
|
public FeedbackExporterEngine(IServiceScopeFactory scopeFactory, IFinlyticLogger<FeedbackExporterEngine> finlyticLogger)
|
||||||
{
|
{
|
||||||
_scopeFactory = scopeFactory;
|
_scopeFactory = scopeFactory;
|
||||||
_logger = logger;
|
_finlyticLogger = finlyticLogger;
|
||||||
_feedbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "feedback");
|
_feedbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "feedback");
|
||||||
|
|
||||||
if (!Directory.Exists(_feedbackDir))
|
if (!Directory.Exists(_feedbackDir))
|
||||||
@@ -46,7 +46,7 @@ public class FeedbackExporterEngine : BackgroundService, IFeedbackExporterEngine
|
|||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("[{Channel}] Feedback Exporter Engine background service started.", "TradesChannel");
|
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[FeedbackExporterEngine] Feedback Exporter Engine background service started.");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -69,7 +69,7 @@ public class FeedbackExporterEngine : BackgroundService, IFeedbackExporterEngine
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "[{Channel}] Error executing feedback exporter job.", "TradesChannel");
|
await _finlyticLogger.LogErrorAsync(SettingKeys.TradesChannel, ex, "[FeedbackExporterEngine] Error executing feedback exporter job.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -82,7 +82,7 @@ public class FeedbackExporterEngine : BackgroundService, IFeedbackExporterEngine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("[{Channel}] Feedback Exporter Engine background service stopped.", "TradesChannel");
|
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[FeedbackExporterEngine] Feedback Exporter Engine background service stopped.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -101,7 +101,7 @@ public class FeedbackExporterEngine : BackgroundService, IFeedbackExporterEngine
|
|||||||
|
|
||||||
if (closedTrades.Count == 0)
|
if (closedTrades.Count == 0)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("[{Channel}] No closed trades available for export.", "TradesChannel");
|
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[FeedbackExporterEngine] No closed trades available for export.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,16 +184,16 @@ public class FeedbackExporterEngine : BackgroundService, IFeedbackExporterEngine
|
|||||||
|
|
||||||
File.Move(parquetTmpPath, parquetPath, overwrite: true);
|
File.Move(parquetTmpPath, parquetPath, overwrite: true);
|
||||||
|
|
||||||
_logger.LogInformation("[{Channel}] Exported Parquet feedback file for sector '{Sector}' to {ParquetPath}", "TradesChannel", sectorName, parquetPath);
|
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[FeedbackExporterEngine] Exported Parquet feedback file for sector '{Sector}' to {ParquetPath}", sectorName, parquetPath);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "[{Channel}] Failed to write Parquet file for sector '{Sector}'. JSON file was written successfully.", "TradesChannel", sectorName);
|
await _finlyticLogger.LogWarningAsync(SettingKeys.TradesChannel, ex, "[FeedbackExporterEngine] Failed to write Parquet file for sector '{Sector}'. JSON file was written successfully.", sectorName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("[{Channel}] Successfully exported feedback data for {Count} closed trades across {Sectors} sectors.",
|
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[FeedbackExporterEngine] Successfully exported feedback data for {Count} closed trades across {Sectors} sectors.",
|
||||||
"TradesChannel", closedTrades.Count, groups.Count());
|
closedTrades.Count, groups.Count());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string SanitizeSectorName(string? sector)
|
private static string SanitizeSectorName(string? sector)
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FinlyticCore.Models.Analyzer;
|
using FinlyticCore.Models.Analyzer;
|
||||||
using FinlyticCore.Models.Trades;
|
using FinlyticCore.Models.Trades;
|
||||||
|
using FinlyticCore.Services;
|
||||||
using FinlyticTrades.Database;
|
using FinlyticTrades.Database;
|
||||||
using FinlyticTrades.Entities;
|
using FinlyticTrades.Entities;
|
||||||
|
using FinlyticTrades.Util;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace FinlyticTrades.Services;
|
namespace FinlyticTrades.Services;
|
||||||
|
|
||||||
@@ -28,19 +29,19 @@ public interface ITradeLifecycleService
|
|||||||
public class TradeLifecycleService : ITradeLifecycleService
|
public class TradeLifecycleService : ITradeLifecycleService
|
||||||
{
|
{
|
||||||
private readonly TradesDbContext _dbContext;
|
private readonly TradesDbContext _dbContext;
|
||||||
private readonly ILogger<TradeLifecycleService> _logger;
|
private readonly IFinlyticLogger<TradeLifecycleService> _finlyticLogger;
|
||||||
|
|
||||||
public TradeLifecycleService(TradesDbContext dbContext, ILogger<TradeLifecycleService> logger)
|
public TradeLifecycleService(TradesDbContext dbContext, IFinlyticLogger<TradeLifecycleService> finlyticLogger)
|
||||||
{
|
{
|
||||||
_dbContext = dbContext;
|
_dbContext = dbContext;
|
||||||
_logger = logger;
|
_finlyticLogger = finlyticLogger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> ProcessManualAnalysisResponseAsync(ManualAnalysisResponseDto response, string userId, CancellationToken cancellationToken = default)
|
public async Task<bool> ProcessManualAnalysisResponseAsync(ManualAnalysisResponseDto response, string userId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (response == null || !response.IsTradeProposed)
|
if (response == null || !response.IsTradeProposed)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("[{Channel}] Manual analysis response indicated NO trade proposed (AnalysisId: {AnalysisId}). Skipping.", "TradesChannel", response?.AnalysisId);
|
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Manual analysis response indicated NO trade proposed (AnalysisId: {AnalysisId}). Skipping.", response?.AnalysisId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(proposal.Symbol) && string.IsNullOrWhiteSpace(proposal.Isin))
|
if (string.IsNullOrWhiteSpace(proposal.Symbol) && string.IsNullOrWhiteSpace(proposal.Isin))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("[{Channel}] ProcessProposedTradeAsync: Received proposal with missing Symbol and ISIN. Skipping.", "TradesChannel");
|
await _finlyticLogger.LogWarningAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] ProcessProposedTradeAsync: Received proposal with missing Symbol and ISIN. Skipping.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,8 +110,8 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
{
|
{
|
||||||
if (existingTrade.Status == TradeStatus.Active)
|
if (existingTrade.Status == TradeStatus.Active)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("[{Channel}] An ACTIVE trade {TradeId} already exists for {Symbol} ({Isin}). Skipping duplicate proposed trade creation.",
|
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] An ACTIVE trade {TradeId} already exists for {Symbol} ({Isin}). Skipping duplicate proposed trade creation.",
|
||||||
"TradesChannel", existingTrade.TradeId, proposal.Symbol, proposal.Isin);
|
existingTrade.TradeId, proposal.Symbol, proposal.Isin);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,8 +124,8 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
_dbContext.Trades.Update(existingTrade);
|
_dbContext.Trades.Update(existingTrade);
|
||||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
_logger.LogInformation("[{Channel}] Successfully UPDATED existing trade proposal {TradeId} for Symbol {Symbol} (ISIN: {Isin}) with status {Status}",
|
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Successfully UPDATED existing trade proposal {TradeId} for Symbol {Symbol} (ISIN: {Isin}) with status {Status}",
|
||||||
"TradesChannel", existingTrade.TradeId, proposal.Symbol, proposal.Isin, existingTrade.Status);
|
existingTrade.TradeId, proposal.Symbol, proposal.Isin, existingTrade.Status);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -143,8 +144,8 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
_dbContext.Trades.Add(tradeEntity);
|
_dbContext.Trades.Add(tradeEntity);
|
||||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
_logger.LogInformation("[{Channel}] Successfully ingested NEW trade proposal {TradeId} for Symbol {Symbol} (ISIN: {Isin}) with status {Status}",
|
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Successfully ingested NEW trade proposal {TradeId} for Symbol {Symbol} (ISIN: {Isin}) with status {Status}",
|
||||||
"TradesChannel", tradeId, proposal.Symbol, proposal.Isin, targetStatus);
|
tradeId, proposal.Symbol, proposal.Isin, targetStatus);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -162,7 +163,7 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
{
|
{
|
||||||
if (existingTrade.Status == TradeStatus.Closed)
|
if (existingTrade.Status == TradeStatus.Closed)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("[{Channel}] Refused to accept trade {TradeId} because its status is CLOSED", "TradesChannel", existingTrade.TradeId);
|
await _finlyticLogger.LogWarningAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Refused to accept trade {TradeId} because its status is CLOSED", existingTrade.TradeId);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +196,7 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
_dbContext.Trades.Update(existingTrade);
|
_dbContext.Trades.Update(existingTrade);
|
||||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
_logger.LogInformation("[{Channel}] Successfully ACCEPTED and UPDATED trade {TradeId} for ISIN {Isin}, UserId: {UserId}", "TradesChannel", existingTrade.TradeId, existingTrade.Isin, existingTrade.UserId);
|
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Successfully ACCEPTED and UPDATED trade {TradeId} for ISIN {Isin}, UserId: {UserId}", existingTrade.TradeId, existingTrade.Isin, existingTrade.UserId);
|
||||||
return existingTrade;
|
return existingTrade;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +262,7 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
_dbContext.Trades.Add(newTrade);
|
_dbContext.Trades.Add(newTrade);
|
||||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
_logger.LogInformation("[{Channel}] Successfully created active trade {TradeId} for ISIN {Isin}, UserId: {UserId}", "TradesChannel", newTrade.TradeId, request.Isin, newTrade.UserId);
|
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Successfully created active trade {TradeId} for ISIN {Isin}, UserId: {UserId}", newTrade.TradeId, request.Isin, newTrade.UserId);
|
||||||
return newTrade;
|
return newTrade;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +273,7 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
|
|
||||||
if (trade == null || (trade.Status != TradeStatus.Active && trade.Status != TradeStatus.Proposed))
|
if (trade == null || (trade.Status != TradeStatus.Active && trade.Status != TradeStatus.Proposed))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("[{Channel}] Cannot add hourly update: Trade {TradeId} not found or not active/proposed.", "TradesChannel", update.TradeId);
|
await _finlyticLogger.LogWarningAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Cannot add hourly update: Trade {TradeId} not found or not active/proposed.", update.TradeId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,16 +306,14 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// NO AUTO CLOSE for active user trades!
|
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Active trade {TradeId} received Close recommendation ({Reasoning}). Trade kept Active for user action.",
|
||||||
// Trade remains Active, alert is stored in HourlyUpdates and surfaced in UI for manual confirmation.
|
trade.TradeId, update.Reasoning);
|
||||||
_logger.LogInformation("[{Channel}] Active trade {TradeId} received Close recommendation ({Reasoning}). Trade kept Active for user action.",
|
|
||||||
"TradesChannel", trade.TradeId, update.Reasoning);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
_logger.LogInformation("[{Channel}] Added hourly update for Trade {TradeId}. Recommendation: {Rec}, Price: {Price}",
|
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Added hourly update for Trade {TradeId}. Recommendation: {Rec}, Price: {Price}",
|
||||||
"TradesChannel", update.TradeId, update.Recommendation, update.CurrentPrice);
|
update.TradeId, update.Recommendation, update.CurrentPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<TradeEntity>> GetActiveTradesAsync(string? userId = null, CancellationToken cancellationToken = default)
|
public async Task<List<TradeEntity>> GetActiveTradesAsync(string? userId = null, CancellationToken cancellationToken = default)
|
||||||
@@ -374,8 +373,8 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
CalculatePnL(trade);
|
CalculatePnL(trade);
|
||||||
|
|
||||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
_logger.LogInformation("[{Channel}] Trade {TradeId} manually closed at price {ExitPrice}. PnL: {PnlAbs} ({PnlPct:F2}%)",
|
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Trade {TradeId} manually closed at price {ExitPrice}. PnL: {PnlAbs} ({PnlPct:F2}%)",
|
||||||
"TradesChannel", trade.TradeId, trade.UserExitPrice, trade.PnlAbsolute, trade.PnlPercent);
|
trade.TradeId, trade.UserExitPrice, trade.PnlAbsolute, trade.PnlPercent);
|
||||||
|
|
||||||
return trade;
|
return trade;
|
||||||
}
|
}
|
||||||
@@ -392,7 +391,7 @@ public class TradeLifecycleService : ITradeLifecycleService
|
|||||||
trade.ClosedAt = DateTime.UtcNow;
|
trade.ClosedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
_logger.LogInformation("[{Channel}] Trade {TradeId} rejected by user.", "TradesChannel", trade.TradeId);
|
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Trade {TradeId} rejected by user.", trade.TradeId);
|
||||||
|
|
||||||
return trade;
|
return trade;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using FinlyticCore.Models.Settings;
|
||||||
|
|
||||||
|
namespace FinlyticTrades.Util;
|
||||||
|
|
||||||
|
public static class SettingKeys
|
||||||
|
{
|
||||||
|
// --- Logging-Kanäle ---
|
||||||
|
public static readonly SettingKey<bool> TradesChannel = new("Logging.Channel.Trades", true);
|
||||||
|
public static readonly SettingKey<bool> MqttChannel = new("Logging.Channel.MQTT", true);
|
||||||
|
public static readonly SettingKey<bool> HealthPingChannel = new("Logging.Channel.Health", true);
|
||||||
|
|
||||||
|
// --- Trade Management & Limits ---
|
||||||
|
public static readonly SettingKey<int> MaxActiveTradesCount = new("Trades.MaxActiveTradesCount", 20);
|
||||||
|
public static readonly SettingKey<int> AutoArchiveClosedTradesDays = new("Trades.AutoArchiveClosedTradesDays", 30);
|
||||||
|
public static readonly SettingKey<double> DefaultSlippageTolerancePercent = new("Trades.DefaultSlippageTolerancePercent", 0.5);
|
||||||
|
public static readonly SettingKey<int> ProposedTradeExpirationHours = new("Trades.ProposedTradeExpirationHours", 24);
|
||||||
|
|
||||||
|
// --- Parquet / Data Export ---
|
||||||
|
public static readonly SettingKey<bool> EnableParquetExport = new("Export.EnableParquetExport", true);
|
||||||
|
public static readonly SettingKey<int> ParquetExportIntervalHours = new("Export.ParquetExportIntervalHours", 6);
|
||||||
|
public static readonly SettingKey<string> ParquetExportDirectory = new("Export.ParquetExportDirectory", "data/exports/trades");
|
||||||
|
}
|
||||||
@@ -6,9 +6,11 @@ using System.Text.Json;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FinlyticCore.Dtos;
|
using FinlyticCore.Dtos;
|
||||||
|
using FinlyticCore.Dtos.Settings;
|
||||||
using FinlyticCore.Dtos.TechnicalAnalysis;
|
using FinlyticCore.Dtos.TechnicalAnalysis;
|
||||||
using FinlyticCore.Models;
|
using FinlyticCore.Models;
|
||||||
using FinlyticCore.Models.Trades;
|
using FinlyticCore.Models.Trades;
|
||||||
|
using FinlyticCore.Services;
|
||||||
using FinlyticCore.Util;
|
using FinlyticCore.Util;
|
||||||
using FinlyticTrades.Entities;
|
using FinlyticTrades.Entities;
|
||||||
using FinlyticTrades.Services;
|
using FinlyticTrades.Services;
|
||||||
@@ -46,19 +48,19 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
ClientId = $"{(_configuration["MQTT:ClientId"] ?? _configuration["MQTT__ClientId"] ?? "finlytic_trades")}_{Guid.NewGuid():N}"
|
ClientId = $"{(_configuration["MQTT:ClientId"] ?? _configuration["MQTT__ClientId"] ?? "finlytic_trades")}_{Guid.NewGuid():N}"
|
||||||
};
|
};
|
||||||
|
|
||||||
_logger.LogInformation("[{Channel}] Starting Unified Trades MQTT Client. Host: {Host}, ClientId: {ClientId}", "TradesChannel", config.Host, config.ClientId);
|
_logger.LogInformation("Starting Unified Trades MQTT Client. Host: {Host}, ClientId: {ClientId}", config.Host, config.ClientId);
|
||||||
await ConnectAsync(config);
|
await ConnectAsync(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StopAsync(CancellationToken cancellationToken)
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("[{Channel}] Stopping Unified Trades MQTT Client.", "TradesChannel");
|
_logger.LogInformation("Stopping Unified Trades MQTT Client.");
|
||||||
await DisconnectAsync();
|
await DisconnectAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnConnectedAsync()
|
protected override async Task OnConnectedAsync()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("[{Channel}] Trades MQTT Client connected. Subscribing to topics...", "TradesChannel");
|
_logger.LogInformation("Trades MQTT Client connected. Subscribing to topics...");
|
||||||
|
|
||||||
await SubscribeAsync("finlytic/trades/proposed/#");
|
await SubscribeAsync("finlytic/trades/proposed/#");
|
||||||
await SubscribeAsync("finlytic/trades/updates/#");
|
await SubscribeAsync("finlytic/trades/updates/#");
|
||||||
@@ -67,11 +69,21 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
await SubscribeAsync("services/request/trades_Close/#");
|
await SubscribeAsync("services/request/trades_Close/#");
|
||||||
await SubscribeAsync("services/request/trades_Reject/#");
|
await SubscribeAsync("services/request/trades_Reject/#");
|
||||||
await SubscribeAsync("services/request/trades_Accept/#");
|
await SubscribeAsync("services/request/trades_Accept/#");
|
||||||
|
await SubscribeAsync("services/request/trades_settings_GetAll/#");
|
||||||
|
await SubscribeAsync("services/request/trades_settings_Update/#");
|
||||||
await SubscribeAsync("services/config/updated/#");
|
await SubscribeAsync("services/config/updated/#");
|
||||||
await SubscribeAsync("services/request/health_Ping/#");
|
await SubscribeAsync("services/request/health_Ping/#");
|
||||||
await SubscribeAsync("services/response/tr_GetLivePrice/#");
|
await SubscribeAsync("services/response/tr_GetLivePrice/#");
|
||||||
|
|
||||||
_logger.LogInformation("[{Channel}] Successfully subscribed to all event and RPC channels.", "TradesChannel");
|
FinlyticCore.Services.FinlyticLogBroadcaster.OnLogPublished = async (logDto) =>
|
||||||
|
{
|
||||||
|
if (IsConnected && string.Equals(logDto.ServiceName, "FinlyticTrades", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await PublishAsync("finlytic/logs/FinlyticTrades", logDto);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("Successfully subscribed to all event and RPC channels.");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnMessageReceivedAsync(string topic, string payloadStr)
|
protected override async Task OnMessageReceivedAsync(string topic, string payloadStr)
|
||||||
@@ -91,7 +103,9 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
string respTopic = $"services/response/health_Ping/{correlationId}";
|
string respTopic = $"services/response/health_Ping/{correlationId}";
|
||||||
var healthResp = new ServiceHealthResponse("FinlyticTrades", "Online", DateTime.UtcNow, "Connected");
|
var healthResp = new ServiceHealthResponse("FinlyticTrades", "Online", DateTime.UtcNow, "Connected");
|
||||||
await PublishAsync(respTopic, healthResp);
|
await PublishAsync(respTopic, healthResp);
|
||||||
_logger.LogInformation("[{Channel}] [TradesMqttClient] Responded to live health_Ping RPC request [CorrelationId: {CorrelationId}].", "TradesChannel", correlationId);
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<TradesMqttClient>>();
|
||||||
|
await finlyticLogger.LogInfoAsync(SettingKeys.HealthPingChannel, "[TradesMqttClient] Responded to live health_Ping RPC request [CorrelationId: {CorrelationId}].", correlationId);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -100,22 +114,35 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
{
|
{
|
||||||
if (topic.EndsWith("FinlyticTrades", StringComparison.OrdinalIgnoreCase))
|
if (topic.EndsWith("FinlyticTrades", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("[{Channel}] [TradesMqttClient] Received config update event for FinlyticTrades.", "TradesChannel");
|
|
||||||
var payload = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.ServiceConfigUpdatePayload);
|
var payload = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.ServiceConfigUpdatePayload);
|
||||||
if (payload?.Settings != null && payload.Settings.Count > 0)
|
if (payload?.Settings != null && payload.Settings.Count > 0)
|
||||||
{
|
{
|
||||||
using var scope = _scopeFactory.CreateScope();
|
using var scope = _scopeFactory.CreateScope();
|
||||||
var settingsDb = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
|
var settings = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
||||||
await settingsDb.UpdateSettingsFromDictionaryAsync(payload.Settings);
|
var dict = payload.Settings.ToDictionary(k => k.Key, v => (object?)v.Value);
|
||||||
_logger.LogInformation("[{Channel}] [TradesMqttClient] Persisted {Count} updated settings to FinlyticTrades database.", "TradesChannel", payload.Settings.Count);
|
await settings.UpdateSettingsAsync(dict);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Für Scoped-Services erzeugen wir pro eingehender Nachricht einen eigenen Scope
|
if (topic.StartsWith("services/request/trades_settings_GetAll", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var correlationId = topic.Split('/').Last();
|
||||||
|
await HandleSettingsGetAllAsync(correlationId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topic.StartsWith("services/request/trades_settings_Update", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var correlationId = topic.Split('/').Last();
|
||||||
|
await HandleSettingsUpdateAsync(payloadStr, correlationId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var msgScope = _scopeFactory.CreateScope();
|
using var msgScope = _scopeFactory.CreateScope();
|
||||||
var tradeLifecycleService = msgScope.ServiceProvider.GetRequiredService<ITradeLifecycleService>();
|
var tradeLifecycleService = msgScope.ServiceProvider.GetRequiredService<ITradeLifecycleService>();
|
||||||
|
var finlyticLoggerInstance = msgScope.ServiceProvider.GetRequiredService<IFinlyticLogger<TradesMqttClient>>();
|
||||||
|
|
||||||
if (topic.StartsWith("finlytic/trades/proposed/"))
|
if (topic.StartsWith("finlytic/trades/proposed/"))
|
||||||
{
|
{
|
||||||
@@ -126,7 +153,7 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogWarning("[{Channel}] [TradesMqttClient] Received proposed trade payload but Symbol/ISIN is empty. Skipping ingestion.", "TradesChannel");
|
await finlyticLoggerInstance.LogWarningAsync(SettingKeys.TradesChannel, "[TradesMqttClient] Received proposed trade payload but Symbol/ISIN is empty. Skipping ingestion.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (topic.StartsWith("finlytic/trades/accept/"))
|
else if (topic.StartsWith("finlytic/trades/accept/"))
|
||||||
@@ -199,7 +226,7 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogDebug(ex, "[{Channel}] Live price fetch skipped or timed out during trades_Get", "TradesChannel");
|
await finlyticLoggerInstance.LogDebugAsync(SettingKeys.TradesChannel, "[TradesMqttClient] Live price fetch skipped or timed out during trades_Get: {Message}", ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +277,72 @@ public class TradesMqttClient : ManagedMqttClient, IHostedService
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "[{Channel}] Error processing incoming MQTT message on topic {Topic}", "TradesChannel", topic);
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<TradesMqttClient>>();
|
||||||
|
await finlyticLogger.LogErrorAsync(SettingKeys.TradesChannel, ex, "[TradesMqttClient] Error processing incoming MQTT message on topic {Topic}", topic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleSettingsGetAllAsync(string correlationId)
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<TradesMqttClient>>();
|
||||||
|
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
||||||
|
|
||||||
|
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTrades] [Settings_GetAll] Retrieving all dynamic settings via reflection [CorrelationId: {CorrelationId}]", correlationId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var settings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) });
|
||||||
|
var responseTopic = $"services/response/trades_settings_GetAll/{correlationId}";
|
||||||
|
|
||||||
|
await PublishAsync(responseTopic, settings);
|
||||||
|
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTrades] [Settings_GetAll] Published {Count} settings to '{ResponseTopic}'", settings.Count, responseTopic);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticTrades] [Settings_GetAll] Failed to retrieve settings.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleSettingsUpdateAsync(string payload, string correlationId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(payload)) return;
|
||||||
|
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<TradesMqttClient>>();
|
||||||
|
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
||||||
|
|
||||||
|
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTrades] [Settings_Update] Processing settings update RPC [CorrelationId: {CorrelationId}]", correlationId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Dictionary<string, object?>? updates = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
updates = JsonSerializer.Deserialize<Dictionary<string, object?>>(payload);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
var list = JsonSerializer.Deserialize<List<DynamicSettingDto>>(payload);
|
||||||
|
if (list != null)
|
||||||
|
{
|
||||||
|
updates = new Dictionary<string, object?>();
|
||||||
|
foreach (var item in list) updates[item.Key] = item.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates != null && updates.Count > 0)
|
||||||
|
{
|
||||||
|
await settingsService.UpdateSettingsAsync(updates);
|
||||||
|
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTrades] [Settings_Update] Successfully updated {Count} settings in database and cache.", updates.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentSettings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) });
|
||||||
|
var responseTopic = $"services/response/trades_settings_Update/{correlationId}";
|
||||||
|
await PublishAsync(responseTopic, currentSettings);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticTrades] [Settings_Update] Failed to update settings.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user